init commit

master
Vic 2 years ago
commit 7d575be3cb

@ -0,0 +1,78 @@
version: 2.1
orbs:
# orgs contain basc recipes and reproducible actions (install node, aws, etc.)
node: circleci/node@5.0.2
eb: circleci/aws-elastic-beanstalk@2.0.1
aws-cli: circleci/aws-cli@3.1.1
# different jobs are calles later in the workflows sections
jobs:
build:
docker:
# the base image can run most needed actions with orbs
- image: "cimg/node:14.15"
steps:
# install node and checkout code
- node/install:
node-version: '14.15'
- checkout
# Use root level package.json to install dependencies in the frontend app
- run:
name: Install Front-End Dependencies
command: |
echo "NODE --version"
echo $(node --version)
echo "NPM --version"
echo $(npm --version)
npm run frontend:install
# TODO: Install dependencies in the the backend API
- run:
name: Install API Dependencies
command: |
echo "TODO: Install dependencies in the the backend API "
# TODO: Lint the frontend
- run:
name: Front-End Lint
command: |
echo "TODO: Lint the frontend"
# TODO: Build the frontend app
- run:
name: Front-End Build
command: |
echo "TODO: Build the frontend app"
# TODO: Build the backend API
- run:
name: API Build
command: |
echo "TODO: Build the backend API"
# deploy step will run only after manual approval
deploy:
docker:
- image: "cimg/base:stable"
# more setup needed for aws, node, elastic beanstalk
steps:
- node/install:
node-version: '14.15'
- eb/setup
- aws-cli/setup
- checkout
- run:
name: Deploy App
# TODO: Install, build, deploy in both apps
command: |
echo "# TODO: Install, build, deploy in both apps"
workflows:
udagram:
jobs:
- build
- hold:
filters:
branches:
only:
- master
type: approval
requires:
- build
- deploy:
requires:
- hold

@ -0,0 +1,46 @@
# Workflow to ensure whenever a Github PR is submitted,
# a JIRA ticket gets created automatically.
name: Manual Workflow
# Controls when the action will run.
on:
# Triggers the workflow on pull request events but only for the master branch
pull_request_target:
types: [opened, reopened]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
test-transition-issue:
name: Convert Github Issue to Jira Issue
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Login
uses: atlassian/gajira-login@master
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
- name: Create NEW JIRA ticket
id: create
uses: atlassian/gajira-create@master
with:
project: CONUPDATE
issuetype: Task
summary: |
Github PR nd0067 - Full Stack JavaScript Developer | Repo: ${{ github.repository }} | PR# ${{github.event.number}}
description: |
Repo link: https://github.com/${{ github.repository }}
PR no. ${{ github.event.pull_request.number }}
PR title: ${{ github.event.pull_request.title }}
PR description: ${{ github.event.pull_request.description }}
In addition, please resolve other issues, if any.
fields: '{"components": [{"name":"nd0067 - Full Stack JavaScript Developer"}], "customfield_16449":"https://classroom.udacity.com/", "customfield_16450":"Resolve the PR", "labels": ["github"], "priority":{"id": "4"}}'
- name: Log created issue
run: echo "Issue ${{ steps.create.outputs.issue }} was created"

@ -0,0 +1 @@
* @udacity/active-public-content

@ -0,0 +1,46 @@
Copyright © 2012 - 2020, Udacity, Inc.
Udacity hereby grants you a license in and to the Educational Content, including
but not limited to homework assignments, programming assignments, code samples,
and other educational materials and tools (as further described in the Udacity
Terms of Use), subject to, as modified herein, the terms and conditions of the
Creative Commons Attribution-NonCommercial- NoDerivs 3.0 License located at
http://creativecommons.org/licenses/by-nc-nd/4.0 and successor locations for
such license (the "CC License") provided that, in each case, the Educational
Content is specifically marked as being subject to the CC License.
Udacity expressly defines the following as falling outside the definition of
"non-commercial":
(a) the sale or rental of (i) any part of the Educational Content, (ii) any
derivative works based at least in part on the Educational Content, or (iii)
any collective work that includes any part of the Educational Content;
(b) the sale of access or a link to any part of the Educational Content without
first obtaining informed consent from the buyer (that the buyer is aware
that the Educational Content, or such part thereof, is available at the
Website free of charge);
(c) providing training, support, or editorial services that use or reference the
Educational Content in exchange for a fee;
(d) the sale of advertisements, sponsorships, or promotions placed on the
Educational Content, or any part thereof, or the sale of advertisements,
sponsorships, or promotions on any website or blog containing any part of
the Educational Material, including without limitation any "pop-up
advertisements";
(e) the use of Educational Content by a college, university, school, or other
educational institution for instruction where tuition is charged; and
(f) the use of Educational Content by a for-profit corporation or non-profit
entity for internal professional development or training.
THE SERVICES AND ONLINE COURSES (INCLUDING ANY CONTENT) ARE PROVIDED "AS IS" AND
"AS AVAILABLE" WITH NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. YOU
ASSUME TOTAL RESPONSIBILITY AND THE ENTIRE RISK FOR YOUR USE OF THE SERVICES,
ONLINE COURSES, AND CONTENT. WITHOUT LIMITING THE FOREGOING, WE DO NOT WARRANT
THAT (A) THE SERVICES, WEBSITES, CONTENT, OR THE ONLINE COURSES WILL MEET YOUR
REQUIREMENTS OR EXPECTATIONS OR ACHIEVE THE INTENDED PURPOSES, (B) THE WEBSITES
OR THE ONLINE COURSES WILL NOT EXPERIENCE OUTAGES OR OTHERWISE BE UNINTERRUPTED,
TIMELY, SECURE OR ERROR-FREE, (C) THE INFORMATION OR CONTENT OBTAINED THROUGH
THE SERVICES, SUCH AS CHAT ROOM SERVICES, WILL BE ACCURATE, COMPLETE, CURRENT,
ERROR- FREE, COMPLETELY SECURE OR RELIABLE, OR (D) THAT DEFECTS IN OR ON THE
SERVICES OR CONTENT WILL BE CORRECTED. YOU ASSUME ALL RISK OF PERSONAL INJURY,
INCLUDING DEATH AND DAMAGE TO PERSONAL PROPERTY, SUSTAINED FROM USE OF SERVICES.

@ -0,0 +1,72 @@
# Hosting a Full-Stack Application
### **You can use you own project completed in previous courses or use the provided Udagram app for completing this final project.**
---
In this project you will learn how to take a newly developed Full-Stack application built for a retailer and deploy it to a cloud service provider so that it is available to customers. You will use the aws console to start and configure the services the application needs such as a database to store product information and a web server allowing the site to be discovered by potential customers. You will modify your package.json scripts and replace hard coded secrets with environment variables in your code.
After the initial setup, you will learn to interact with the services you started on aws and will deploy manually the application a first time to it. As you get more familiar with the services and interact with them through a CLI, you will gradually understand all the moving parts.
You will then register for a free account on CircleCi and connect your Github account to it. Based on the manual steps used to deploy the app, you will write a config.yml file that will make the process reproducible in CircleCi. You will set up the process to be executed automatically based when code is pushed on the main Github branch.
The project will also include writing documentation and runbooks covering the operations of the deployment process. Those runbooks will serve as a way to communicate with future developers and anybody involved in diagnosing outages of the Full-Stack application.
# Udagram
This application is provided to you as an alternative starter project if you do not wish to host your own code done in the previous courses of this nanodegree. The udagram application is a fairly simple application that includes all the major components of a Full-Stack web application.
### Dependencies
```
- Node v14.15.1 (LTS) or more recent. While older versions can work it is advisable to keep node to latest LTS version
- npm 6.14.8 (LTS) or more recent, Yarn can work but was not tested for this project
- AWS CLI v2, v1 can work but was not tested for this project
- A RDS database running Postgres.
- A S3 bucket for hosting uploaded pictures.
```
### Installation
Provision the necessary AWS services needed for running the application:
1. In AWS, provision a publicly available RDS database running Postgres. <Place holder for link to classroom article>
1. In AWS, provision a s3 bucket for hosting the uploaded files. <Place holder for tlink to classroom article>
1. Export the ENV variables needed or use a package like [dotnev](https://www.npmjs.com/package/dotenv)/.
1. From the root of the repo, navigate udagram-api folder `cd starter/udagram-api` to install the node_modules `npm install`. After installation is done start the api in dev mode with `npm run dev`.
1. Without closing the terminal in step 1, navigate to the udagram-frontend `cd starter/udagram-frontend` to intall the node_modules `npm install`. After installation is done start the api in dev mode with `npm run start`.
## Testing
This project contains two different test suite: unit tests and End-To-End tests(e2e). Follow these steps to run the tests.
1. `cd starter/udagram-frontend`
1. `npm run test`
1. `npm run e2e`
There are no Unit test on the back-end
### Unit Tests:
Unit tests are using the Jasmine Framework.
### End to End Tests:
The e2e tests are using Protractor and Jasmine.
## Built With
- [Angular](https://angular.io/) - Single Page Application Framework
- [Node](https://nodejs.org) - Javascript Runtime
- [Express](https://expressjs.com/) - Javascript API Framework
## License
[License](LICENSE.txt)

@ -0,0 +1,16 @@
{
"scripts": {
"frontend:install": "cd udagram/udagram-frontend && npm install -f",
"frontend:start": "cd udagram/udagram-frontend && npm run start",
"frontend:build": "cd udagram/udagram-frontend && npm run build",
"frontend:test": "cd udagram/udagram-frontend && npm run test",
"frontend:e2e": "cd udagram/udagram-frontend && npm run e2e",
"frontend:lint": "cd udagram/udagram-frontend && npm run lint",
"frontend:deploy": "cd udagram/udagram-frontend && npm run deploy",
"api:install": "cd udagram/udagram-api && npm install .",
"api:build": "cd udagram/udagram-api && npm run build",
"api:start": "cd udagram/udagram-api && npm run dev",
"api:deploy": "cd udagram/udagram-api && npm run deploy",
"deploy": "npm run api:deploy && npm run frontend:deploy"
}
}

@ -0,0 +1 @@
node_modules

@ -0,0 +1,11 @@
# This file is used for convenience of local development.
# DO NOT STORE YOUR CREDENTIALS INTO GIT
export POSTGRES_USERNAME=postgres
export POSTGRES_PASSWORD=myPassword
export POSTGRES_HOST=mydbinstance.csxbuclmtj3c.us-east-1.rds.amazonaws.com
export POSTGRES_DB=postgres
export AWS_BUCKET=arn:aws:s3:::myawsbucket-75139724085
export AWS_REGION=us-east-1
export AWS_PROFILE=default
export JWT_SECRET=mysecretstring
export URL=http://localhost:8100

@ -0,0 +1,26 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}

@ -0,0 +1,43 @@
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore
*~
*.sw[mnpcod]
*.log
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
.vscode/
npm-debug.log*
.idea/
.ionic/
.sourcemaps/
.sass-cache/
.tmp/
.versions/
coverage/
www/
node_modules/
tmp/
temp/
platforms/
plugins/
plugins/android.json
plugins/ios.json
$RECYCLE.BIN/
postgres_dev/
logfile
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
node_modules
venv/
.env
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml

@ -0,0 +1 @@
unsafe-perm=true

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

File diff suppressed because it is too large Load Diff

@ -0,0 +1,55 @@
{
"name": "udagram-api",
"version": "2.0.0",
"description": "",
"engines": {
"node": "14.15.0"
},
"main": "src/server.ts",
"scripts": {
"start": "node ./www/server.js",
"tsc": "npx tsc",
"dev": "npx ts-node-dev --respawn --transpile-only ./src/server.ts",
"prod": "npx tsc && node ./www/server.js",
"clean": "rm -rf www/ || true",
"deploy": "npm run build && eb list && eb use udagram-api-dev && eb deploy",
"build": "npm install . && npm run clean && tsc && cp -rf src/config www/config && cp -R .elasticbeanstalk www/.elasticbeanstalk && cp .npmrc www/.npmrc && cp package.json www/package.json && cd www && zip -r Archive.zip . && cd ..",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Gabriel Ruttner",
"license": "ISC",
"dependencies": {
"@types/bcryptjs": "2.4.2",
"@types/jsonwebtoken": "^8.3.2",
"aws-sdk": "^2.429.0",
"bcryptjs": "2.4.3",
"body-parser": "^1.18.3",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"email-validator": "^2.0.4",
"express": "^4.16.4",
"jsonwebtoken": "^8.5.1",
"pg": "^8.7.1",
"reflect-metadata": "^0.1.13",
"sequelize": "^5.21.4",
"sequelize-typescript": "^0.6.9"
},
"devDependencies": {
"@types/bluebird": "^3.5.26",
"@types/cors": "^2.8.6",
"@types/express": "^4.16.1",
"@types/node": "^11.11.6",
"@types/sequelize": "^4.27.44",
"@types/validator": "^10.9.0",
"@typescript-eslint/eslint-plugin": "^2.19.2",
"@typescript-eslint/parser": "^2.19.2",
"chai": "^4.2.0",
"chai-http": "^4.2.1",
"eslint": "^6.8.0",
"eslint-config-google": "^0.14.0",
"mocha": "^6.1.4",
"ts-node-dev": "^1.0.0-pre.32",
"typescript": "^3.9.10"
}
}

@ -0,0 +1,32 @@
import AWS = require("aws-sdk");
import { config } from "./config/config";
//Credentials are auto set according to the documentation https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html and the default profile is "Default anyway"
export const s3 = new AWS.S3({
signatureVersion: "v4",
region: config.aws_region,
params: { Bucket: config.aws_media_bucket },
});
// Generates an AWS signed URL for retrieving objects
export function getGetSignedUrl(key: string): string {
const signedUrlExpireSeconds = 60 * 5;
return s3.getSignedUrl("getObject", {
Bucket: config.aws_media_bucket,
Key: key,
Expires: signedUrlExpireSeconds,
});
}
// Generates an AWS signed URL for uploading objects
export function getPutSignedUrl(key: string): string {
const signedUrlExpireSeconds = 60 * 5;
return s3.getSignedUrl("putObject", {
Bucket: config.aws_media_bucket,
Key: key,
Expires: signedUrlExpireSeconds,
});
}

@ -0,0 +1,16 @@
## Environment Variables Explaination
You need a separate S3 Media bucket !
`POSTGRES_HOST` : Your Postgres DB host
`POSTGRES_USERNAME` : Your Postgres DB username
`POSTGRES_DB` : Your Postgres DB username
`POSTGRES_PASSWORD` : Your Postgres DB username
`PORT` : Currently set to BOTH DB port && application port *Needs fix*
`AWS_REGION` : Your MEDIA bucket AWS region EG.: "eu-west-3"
`AWS_PROFILE` : Set when setting up AWS CLI, by default should be set up to "default"
`AWS_BUCKET` : Your media bucket name EG.: "mediabucket123123"
`URL` : Your backend URL, can be found after creating EB Environment
`JWT_SECRET` : Your JWT token secret, can be set to any value

@ -0,0 +1,23 @@
import * as dotenv from "dotenv";
dotenv.config();
// ENV variables
// - AWS_ACCESS_KEY_ID
// - AWS_SECRET_ACCESS_KEY
// Are Also needed
export const config = {
username: `${process.env.POSTGRES_USERNAME}`,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
port: Number(process.env.PORT),
host: process.env.POSTGRES_HOST,
dialect: "postgres",
aws_region: process.env.AWS_REGION,
aws_profile: process.env.AWS_PROFILE,
aws_media_bucket: process.env.AWS_BUCKET,
url: process.env.URL,
jwt: {
secret: process.env.JWT_SECRET,
},
};

@ -0,0 +1,19 @@
import {Table, Column, Model, CreatedAt, UpdatedAt} from 'sequelize-typescript';
@Table
export class FeedItem extends Model<FeedItem> {
@Column
public caption!: string;
@Column
public url!: string;
@Column
@CreatedAt
public createdAt: Date = new Date();
@Column
@UpdatedAt
public updatedAt: Date = new Date();
}

@ -0,0 +1,83 @@
import {Router, Request, Response} from 'express';
import {FeedItem} from '../models/FeedItem';
import {NextFunction} from 'connect';
import * as jwt from 'jsonwebtoken';
import * as AWS from '../../../../aws';
import * as c from '../../../../config/config';
const router: Router = Router();
export function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.headers || !req.headers.authorization) {
return res.status(401).send({message: 'No authorization headers.'});
}
const tokenBearer = req.headers.authorization.split(' ');
if (tokenBearer.length != 2) {
return res.status(401).send({message: 'Malformed token.'});
}
const token = tokenBearer[1];
return jwt.verify(token, c.config.jwt.secret, (err, decoded) => {
if (err) {
return res.status(500).send({auth: false, message: 'Failed to authenticate.'});
}
return next();
});
}
// Get all feed items
router.get('/', async (req: Request, res: Response) => {
const items = await FeedItem.findAndCountAll({order: [['id', 'DESC']]});
items.rows.map((item) => {
if (item.url) {
item.url = AWS.getGetSignedUrl(item.url);
}
});
res.send(items);
});
// Get a feed resource
router.get('/:id',
async (req: Request, res: Response) => {
const {id} = req.params;
const item = await FeedItem.findByPk(id);
res.send(item);
});
// Get a signed url to put a new item in the bucket
router.get('/signed-url/:fileName',
requireAuth,
async (req: Request, res: Response) => {
const {fileName} = req.params;
const url = AWS.getPutSignedUrl(fileName);
res.status(201).send({url: url});
});
// Create feed with metadata
router.post('/',
requireAuth,
async (req: Request, res: Response) => {
const caption = req.body.caption;
const fileName = req.body.url; // same as S3 key name
if (!caption) {
return res.status(400).send({message: 'Caption is required or malformed.'});
}
if (!fileName) {
return res.status(400).send({message: 'File url is required.'});
}
const item = await new FeedItem({
caption: caption,
url: fileName,
});
const savedItem = await item.save();
savedItem.url = AWS.getGetSignedUrl(savedItem.url);
res.status(201).send(savedItem);
});
export const FeedRouter: Router = router;

@ -0,0 +1,14 @@
import {Router, Request, Response} from 'express';
import {FeedRouter} from './feed/routes/feed.router';
import {UserRouter} from './users/routes/user.router';
const router: Router = Router();
router.use('/feed', FeedRouter);
router.use('/users', UserRouter);
router.get('/', async (req: Request, res: Response) => {
res.send(`V0`);
});
export const IndexRouter: Router = router;

@ -0,0 +1,6 @@
import {FeedItem} from './feed/models/FeedItem';
import {User} from './users/models/User';
export const V0_USER_MODELS = [User];
export const V0_FEED_MODELS = [FeedItem];

@ -0,0 +1,25 @@
import {Table, Column, Model, PrimaryKey, CreatedAt, UpdatedAt} from 'sequelize-typescript';
@Table
export class User extends Model<User> {
@PrimaryKey
@Column
public email!: string;
@Column
public passwordHash!: string;
@Column
@CreatedAt
public createdAt: Date = new Date();
@Column
@UpdatedAt
public updatedAt: Date = new Date();
short() {
return {
email: this.email,
};
}
}

@ -0,0 +1,118 @@
import {Router, Request, Response} from 'express';
import {User} from '../models/User';
import * as c from '../../../../config/config';
// import * as bcrypt from 'bcrypt';
import * as jwt from 'jsonwebtoken';
import {NextFunction} from 'connect';
import * as EmailValidator from 'email-validator';
import {config} from 'bluebird';
const router: Router = Router();
var bcrypt = require('bcryptjs');
async function generatePassword(plainTextPassword: string): Promise<string> {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
return await bcrypt.hash(plainTextPassword, salt);
}
async function comparePasswords(plainTextPassword: string, hash: string): Promise<boolean> {
return await bcrypt.compare(plainTextPassword, hash);
}
function generateJWT(user: User): string {
return jwt.sign(user.short(), c.config.jwt.secret);
}
export function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.headers || !req.headers.authorization) {
return res.status(401).send({message: 'No authorization headers.'});
}
const tokenBearer = req.headers.authorization.split(' ');
if (tokenBearer.length != 2) {
return res.status(401).send({message: 'Malformed token.'});
}
const token = tokenBearer[1];
return jwt.verify(token, c.config.jwt.secret, (err, decoded) => {
if (err) {
return res.status(500).send({auth: false, message: 'Failed to authenticate.'});
}
return next();
});
}
router.get('/verification',
requireAuth,
async (req: Request, res: Response) => {
return res.status(200).send({auth: true, message: 'Authenticated.'});
});
router.post('/login', async (req: Request, res: Response) => {
const email = req.body.email;
const password = req.body.password;
if (!email || !EmailValidator.validate(email)) {
return res.status(400).send({auth: false, message: 'Email is required or malformed.'});
}
if (!password) {
return res.status(400).send({auth: false, message: 'Password is required.'});
}
const user = await User.findByPk(email);
if (!user) {
return res.status(401).send({auth: false, message: 'User was not found..'});
}
const authValid = await comparePasswords(password, user.passwordHash);
if (!authValid) {
return res.status(401).send({auth: false, message: 'Password was invalid.'});
}
const jwt = generateJWT(user);
res.status(200).send({auth: true, token: jwt, user: user.short()});
});
router.post('/', async (req: Request, res: Response) => {
const email = req.body.email;
const plainTextPassword = req.body.password;
if (!email || !EmailValidator.validate(email)) {
return res.status(400).send({auth: false, message: 'Email is missing or malformed.'});
}
if (!plainTextPassword) {
return res.status(400).send({auth: false, message: 'Password is required.'});
}
const user = await User.findByPk(email);
if (user) {
return res.status(422).send({auth: false, message: 'User already exists.'});
}
const generatedHash = await generatePassword(plainTextPassword);
const newUser = await new User({
email: email,
passwordHash: generatedHash,
});
const savedUser = await newUser.save();
const jwt = generateJWT(savedUser);
res.status(201).send({token: jwt, user: savedUser.short()});
});
router.get('/', async (req: Request, res: Response) => {
res.send('auth');
});
export const AuthRouter: Router = router;

@ -0,0 +1,18 @@
import {Router, Request, Response} from 'express';
import {User} from '../models/User';
import {AuthRouter} from './auth.router';
const router: Router = Router();
router.use('/auth', AuthRouter);
router.get('/');
router.get('/:id', async (req: Request, res: Response) => {
const {id} = req.params;
const item = await User.findByPk(id);
res.send(item);
});
export const UserRouter: Router = router;

@ -0,0 +1,30 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('FeedItem', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
caption: {
type: Sequelize.STRING,
},
url: {
type: Sequelize.STRING,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('FeedItem');
},
};

@ -0,0 +1,30 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('User', {
id: {
allowNull: false,
autoIncrement: true,
type: Sequelize.INTEGER,
},
email: {
type: Sequelize.STRING,
primaryKey: true,
},
passwordHash: {
type: Sequelize.STRING,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('User');
},
};

@ -0,0 +1,12 @@
import { Sequelize } from "sequelize-typescript";
import { config } from "./config/config";
export const sequelize = new Sequelize({
username: config.username,
password: config.password,
database: config.database,
host: config.host,
dialect: "postgres",
storage: ":memory:",
});

@ -0,0 +1,54 @@
import * as dotenv from "dotenv";
import cors from 'cors';
import express from "express";
import { sequelize } from "./sequelize";
import { IndexRouter } from "./controllers/v0/index.router";
import bodyParser from "body-parser";
import { V0_FEED_MODELS, V0_USER_MODELS } from "./controllers/v0/model.index";
(async () => {
dotenv.config();
await sequelize.addModels(V0_FEED_MODELS);
await sequelize.addModels(V0_USER_MODELS);
await sequelize.sync();
console.log("Database Connected");
const app = express();
const port = process.env.PORT || 8080;
app.use(bodyParser.json());
// app.use(cors());
// We set the CORS origin to * so that we don't need to
// worry about the complexities of CORS.
app.use(cors({
"allowedHeaders": [
'Origin', 'X-Requested-With',
'Content-Type', 'Accept',
'X-Access-Token', 'Authorization', 'Access-Control-Allow-Origin',
'Access-Control-Allow-Headers',
'Access-Control-Allow-Methods'
],
"methods": 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
"preflightContinue": true,
"origin": '*',
}));
app.use("/api/v0/", IndexRouter);
// Root URI call
app.get("/", async (req, res) => {
res.send("/api/v0/");
});
// Start the Server
app.listen(port, () => {
console.log(`Backend server is listening on port ${port}....`);
console.log(`Frontent server running ${process.env.URL}`);
console.log(`press CTRL+C to stop server`);
});
})();

@ -0,0 +1,67 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./www", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": false, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "/", /* Base directory to resolve non-absolute module names. */
"paths": {
"*": [
"node_modules/*"
]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": [
"src/**/*"
]
}

@ -0,0 +1,139 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"page",
"kebab-case"
],
"no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"directive-class-suffix": true
}
}

@ -0,0 +1,25 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}

@ -0,0 +1,35 @@
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore
*~
*.sw[mnpcod]
*.log
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
.vscode/
npm-debug.log*
.idea/
.ionic/
.sourcemaps/
.sass-cache/
.tmp/
.versions/
coverage/
www/
node_modules/
tmp/
temp/
platforms/
plugins/
plugins/android.json
plugins/ios.json
$RECYCLE.BIN/
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
node_modules

@ -0,0 +1,194 @@
{
"$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json",
"version": 1,
"defaultProject": "app",
"newProjectRoot": "projects",
"projects": {
"app": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "www",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
}
],
"styles": [
{
"input": "src/theme/variables.scss"
},
{
"input": "src/global.scss"
}
],
"scripts": [],
"es5BrowserSupport": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"ci": {
"progress": false
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "app:build"
},
"configurations": {
"production": {
"browserTarget": "app:build:production"
},
"ci": {
"progress": false
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [],
"scripts": [],
"assets": [
{
"glob": "favicon.ico",
"input": "src/",
"output": "/"
},
{
"glob": "**/*",
"input": "src/assets",
"output": "/assets"
}
]
},
"configurations": {
"ci": {
"progress": false,
"watch": false
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
},
"ionic-cordova-build": {
"builder": "@ionic/angular-toolkit:cordova-build",
"options": {
"browserTarget": "app:build"
},
"configurations": {
"production": {
"browserTarget": "app:build:production"
}
}
},
"ionic-cordova-serve": {
"builder": "@ionic/angular-toolkit:cordova-serve",
"options": {
"cordovaBuildTarget": "app:ionic-cordova-build",
"devServerTarget": "app:serve"
},
"configurations": {
"production": {
"cordovaBuildTarget": "app:ionic-cordova-build:production",
"devServerTarget": "app:serve:production"
}
}
}
}
},
"app-e2e": {
"root": "e2e/",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "app:serve"
},
"configurations": {
"ci": {
"devServerTarget": "app:serve:ci"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": ["**/node_modules/**"]
}
}
}
}
},
"cli": {
"defaultCollection": "@ionic/angular-toolkit"
},
"schematics": {
"@ionic/angular-toolkit:component": {
"styleext": "scss"
},
"@ionic/angular-toolkit:page": {
"styleext": "scss"
}
}
}

@ -0,0 +1,2 @@
aws s3 cp --recursive --acl public-read ./www s3://myawsbucket-75139724085/
aws s3 cp --acl public-read --cache-control="max-age=0, no-cache, no-store, must-revalidate" ./www/index.html s3://myawsbucket-75139724085/

@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

@ -0,0 +1,19 @@
import { AppPage } from './app.po';
describe('new App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
describe('default screen', () => {
beforeEach(() => {
page.navigateTo('/home');
});
it('should have a title saying Home', () => {
page.getPageOneTitleText().then(title => {
expect(title).toEqual('Home');
});
});
});
});

@ -0,0 +1,15 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(destination) {
return browser.get(destination);
}
getTitle() {
return browser.getTitle();
}
getPageOneTitleText() {
return element(by.tagName('app-home')).element(by.deepCss('ion-title')).getText();
}
}

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

@ -0,0 +1,5 @@
{
"name": "udacity-c2-frontend",
"integrations": {},
"type": "angular"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,61 @@
{
"name": "udagram-frontend",
"version": "0.0.1",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"deploy": "npm install -f && npm run build && chmod +x bin/deploy.sh && bin/deploy.sh",
"test": "ng test --watch=false",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/common": "^8.2.14",
"@angular/core": "^8.2.14",
"@angular/forms": "^8.2.14",
"@angular/http": "^7.2.16",
"@angular/platform-browser": "^8.2.14",
"@angular/platform-browser-dynamic": "^8.2.14",
"@angular/router": "^8.2.14",
"@ionic-native/core": "^5.0.0",
"@ionic-native/splash-screen": "^5.0.0",
"@ionic-native/status-bar": "^5.0.0",
"@ionic/angular": "^4.1.0",
"core-js": "^2.5.4",
"rxjs": "~6.5.4",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/architect": "~0.12.3",
"@angular-devkit/build-angular": "^0.803.24",
"@angular-devkit/core": "~7.2.3",
"@angular-devkit/schematics": "~7.2.3",
"@angular/cli": "~8.3.25",
"@angular/compiler": "~8.2.14",
"@angular/compiler-cli": "~8.2.14",
"@angular/language-service": "~8.2.14",
"@ionic/angular-toolkit": "~1.4.0",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~10.12.0",
"@typescript-eslint/eslint-plugin": "^2.20.0",
"@typescript-eslint/parser": "^2.20.0",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.1.4",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"ts-node": "~8.0.0",
"tslint": "~5.12.0",
"typescript": "^3.5.3"
},
"description": "An Ionic project"
}

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
const components = [];
@NgModule({
imports: [
HttpClientModule,
],
declarations: components,
exports: components,
providers: []
})
export class ApiModule {}

@ -0,0 +1,75 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpRequest, HttpEvent } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { map } from 'rxjs/operators';
const API_HOST = environment.apiHost;
@Injectable({
providedIn: 'root'
})
export class ApiService {
httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};
token: string;
constructor(private http: HttpClient) {
}
static handleError(error: Error) {
alert(error.message);
}
static extractData(res: HttpEvent<any>) {
const body = res;
return body || { };
}
setAuthToken(token) {
this.httpOptions.headers = this.httpOptions.headers.append('Authorization', `jwt ${token}`);
this.token = token;
}
get(endpoint): Promise<any> {
const url = `${API_HOST}${endpoint}`;
const req = this.http.get(url, this.httpOptions).pipe(map(ApiService.extractData));
return req
.toPromise()
.catch((e) => {
ApiService.handleError(e);
throw e;
});
}
post(endpoint, data): Promise<any> {
const url = `${API_HOST}${endpoint}`;
return this.http.post<HttpEvent<any>>(url, data, this.httpOptions)
.toPromise()
.catch((e) => {
ApiService.handleError(e);
throw e;
});
}
async upload(endpoint: string, file: File, payload: any): Promise<any> {
const signed_url = (await this.get(`${endpoint}/signed-url/${file.name}`)).url;
const headers = new HttpHeaders({'Content-Type': file.type});
const req = new HttpRequest( 'PUT', signed_url, file,
{
headers: headers,
reportProgress: true, // track progress
});
return new Promise ( resolve => {
this.http.request(req).subscribe((resp) => {
if (resp && (<any> resp).status && (<any> resp).status === 200) {
resolve(this.post(endpoint, payload));
}
});
});
}
}

@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
loadChildren: './home/home.module#HomePageModule'
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule {}

@ -0,0 +1,32 @@
<ion-app>
<ion-split-pane when="false">
<ion-menu>
<ion-header>
<ion-toolbar>
<ion-title>Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-menu-toggle auto-hide="false" *ngFor="let p of appPages">
<ion-item [routerDirection]="'root'" [routerLink]="[p.url]">
<ion-icon slot="start" [name]="p.icon"></ion-icon>
<ion-label>
{{p.title}}
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
</ion-content>
</ion-menu>
<ion-content main>
<app-menubar></app-menubar>
<ion-router-outlet style="margin-top: 56px;"></ion-router-outlet>
</ion-content>
</ion-split-pane>
</ion-app>

@ -0,0 +1,81 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy;
beforeEach(async(() => {
statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']);
splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']);
platformReadySpy = Promise.resolve();
platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy });
TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers: [
{ provide: StatusBar, useValue: statusBarSpy },
{ provide: SplashScreen, useValue: splashScreenSpy },
{ provide: Platform, useValue: platformSpy },
],
imports: [ RouterTestingModule.withRoutes([])],
}).compileComponents();
}));
it('should create the app', async () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it('should initialize the app', async () => {
TestBed.createComponent(AppComponent);
expect(platformSpy.ready).toHaveBeenCalled();
await platformReadySpy;
expect(statusBarSpy.styleDefault).toHaveBeenCalled();
expect(splashScreenSpy.hide).toHaveBeenCalled();
});
it('should have menu labels', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const menuItems = app.querySelectorAll('ion-label');
expect(menuItems.length).toEqual(1);
expect(menuItems[0].textContent).toContain('Home');
});
it('should have urls', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const menuItems = app.querySelectorAll('ion-item');
expect(menuItems.length).toEqual(1);
expect(menuItems[0].getAttribute('ng-reflect-router-link')).toEqual('/home');
});
it('should have one router outlet', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const routerOutlet = app.querySelectorAll('ion-router-outlet');
expect(routerOutlet.length).toEqual(1);
});
it('should have one menubar', async () => {
const fixture = await TestBed.createComponent(AppComponent);
await fixture.detectChanges();
const app = fixture.nativeElement;
const menubar = app.querySelectorAll('app-menubar');
expect(menubar.length).toEqual(1);
});
});

@ -0,0 +1,38 @@
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
public appPages = [
{
title: 'Home',
url: '/home',
icon: 'home'
}
];
public appName = environment.appName;
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar
) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
document.title = environment.appName;
});
}
}

@ -0,0 +1,36 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { MenubarComponent } from './menubar/menubar.component';
import { AuthModule } from './auth/auth.module';
import { ApiService } from './api/api.service';
@NgModule({
declarations: [
AppComponent,
MenubarComponent
],
entryComponents: [],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
AuthModule
],
providers: [
ApiService,
StatusBar,
SplashScreen,
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {}

@ -0,0 +1,17 @@
<form [formGroup]="loginForm" (submit)="onSubmit($event)">
<ion-item>
<ion-label position="floating" color="primary">Email</ion-label>
<ion-input type="text" formControlName="email" required></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating" color="primary">Password</ion-label>
<ion-input type="password" formControlName="password" required></ion-input>
</ion-item>
<ion-button
color="primary"
type="submit"
[disabled]="!loginForm.valid">
Log In
</ion-button>
{{error}}
</form>

@ -0,0 +1,50 @@
import { Component, OnInit } from '@angular/core';
import { Validators, FormBuilder, FormGroup, FormControl } from '@angular/forms';
import { ModalController } from '@ionic/angular';
import { AuthService } from '../services/auth.service';
@Component({
selector: 'app-auth-login',
templateUrl: './auth-login.component.html',
styleUrls: ['./auth-login.component.scss'],
})
export class AuthLoginComponent implements OnInit {
loginForm: FormGroup;
error: string;
constructor(
private formBuilder: FormBuilder,
private auth: AuthService,
private modal: ModalController
) { }
ngOnInit() {
this.loginForm = this.formBuilder.group({
password: new FormControl('', Validators.required),
email: new FormControl('', Validators.compose([
Validators.required,
Validators.pattern('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$')
]))
});
}
async onSubmit($event) {
$event.preventDefault();
if (!this.loginForm.valid) { return; }
this.auth.login(
this.loginForm.controls.email.value,
this.loginForm.controls.password.value)
.then((user) => {
this.modal.dismiss();
})
.catch((e) => {
this.error = e.statusText;
throw e;
});
}
}

@ -0,0 +1,28 @@
<!-- display login and register buttons if user is logged in -->
<ion-item-group
*ngIf=" !( auth.currentUser$ | async ) ">
<ion-button
color="secondary"
(click)="presentRegister()">
Register
</ion-button>
<ion-button
(click)="presentLogin()"
color="primary">
Log In
</ion-button>
</ion-item-group>
<!-- display the avatar if user is logged in -->
<ion-item-group
*ngIf=" ( auth.currentUser$ | async ) ">
<ion-button
color="secondary">
{{( auth.currentUser$ | async ).email }}
</ion-button>
<ion-button
(click)="logout()"
color="primary">
Log Out
</ion-button>
</ion-item-group>

@ -0,0 +1,11 @@
:host{
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
ion-avatar {
width: 35px;
height: 35px;
}

@ -0,0 +1,48 @@
import { Component, OnInit } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { AuthMenuUserComponent } from './auth-menu-user/auth-menu-user.component';
import { AuthService } from '../services/auth.service';
import { AuthLoginComponent } from '../auth-login/auth-login.component';
import { AuthRegisterComponent } from '../auth-register/auth-register.component';
@Component({
selector: 'app-auth-menu-button',
templateUrl: './auth-menu-button.component.html',
styleUrls: ['./auth-menu-button.component.scss'],
})
export class AuthMenuButtonComponent implements OnInit {
constructor(
private auth: AuthService,
public modalController: ModalController
) {}
async presentmodal(ev: any) {
const modal = await this.modalController.create({
component: AuthMenuUserComponent,
});
return await modal.present();
}
async presentLogin(ev: any) {
const modal = await this.modalController.create({
component: AuthLoginComponent,
});
return await modal.present();
}
async presentRegister(ev: any) {
const modal = await this.modalController.create({
component: AuthRegisterComponent,
});
return await modal.present();
}
logout() {
this.auth.logout();
}
ngOnInit() {}
}

@ -0,0 +1,5 @@
<ion-button color="secondary" (click)="dismissModal()">Dismiss</ion-button>
<p>
auth-menu-user works!
</p>

@ -0,0 +1,42 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthMenuUserComponent } from './auth-menu-user.component';
import { ModalController } from '@ionic/angular';
describe('AuthMenuUserPage', () => {
let component: AuthMenuUserComponent;
let fixture: ComponentFixture<AuthMenuUserComponent>;
let modalSpy;
let modalCtrlSpy;
beforeEach(async(() => {
modalSpy = jasmine.createSpyObj('Modal', ['dismiss']);
modalCtrlSpy = jasmine.createSpyObj('ModalController', ['create']);
modalCtrlSpy.create.and.callFake(function () {
return modalSpy;
});
TestBed.configureTestingModule({
providers: [
{
provide: ModalController,
useValue: modalCtrlSpy
}
],
declarations: [ AuthMenuUserComponent ],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthMenuUserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { ModalController } from '@ionic/angular';
@Component({
selector: 'app-auth-menu-user',
templateUrl: './auth-menu-user.component.html',
styleUrls: ['./auth-menu-user.component.scss'],
})
export class AuthMenuUserComponent implements OnInit {
constructor(private modalCtrl: ModalController) { }
ngOnInit() {}
dismissModal() {
this.modalCtrl.dismiss();
}
}

@ -0,0 +1,25 @@
<form [formGroup]="registerForm" (submit)="onSubmit($event)">
<ion-item>
<ion-label position="floating" color="primary">Name</ion-label>
<ion-input type="text" formControlName="name" required></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating" color="primary">Email</ion-label>
<ion-input type="text" formControlName="email" required></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating" color="primary">Password</ion-label>
<ion-input type="password" formControlName="password" required></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating" color="primary">Confirm Password</ion-label>
<ion-input type="password" formControlName="password_confirm" required></ion-input>
</ion-item>
<ion-button
color="primary"
type="submit"
[disabled]="!registerForm.valid ">
Register
</ion-button> {{error}}
</form>

@ -0,0 +1,61 @@
import { Component, OnInit } from '@angular/core';
import { Validators, FormBuilder, FormGroup, FormControl } from '@angular/forms';
import { AuthService } from '../services/auth.service';
import { User } from '../models/user.model';
import { ModalController } from '@ionic/angular';
@Component({
selector: 'app-auth-register',
templateUrl: './auth-register.component.html',
styleUrls: ['./auth-register.component.scss'],
})
export class AuthRegisterComponent implements OnInit {
registerForm: FormGroup;
error: string;
constructor(
private formBuilder: FormBuilder,
private auth: AuthService,
private modal: ModalController
) { }
ngOnInit() {
this.registerForm = this.formBuilder.group({
password_confirm: new FormControl('', Validators.required),
password: new FormControl('', Validators.required),
email: new FormControl('', Validators.compose([
Validators.required,
Validators.pattern('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$')
])),
name: new FormControl('', Validators.compose([
Validators.required,
Validators.pattern('^[a-zA-Z0-9_.+-]+$')
]))
}, { validators: this.passwordsMatch });
}
onSubmit($event) {
$event.preventDefault();
if (!this.registerForm.valid) { return; }
const newuser: User = {
email: this.registerForm.controls.email.value,
name: this.registerForm.controls.name.value
};
this.auth.register(newuser, this.registerForm.controls.password.value)
.then((user) => {
this.modal.dismiss();
})
.catch((e) => {
this.error = e.statusText;
throw e;
});
}
passwordsMatch(group: FormGroup) {
return group.controls.password.value === group.controls.password_confirm.value ? null : { passwordsMisMatch: true };
}
}

@ -0,0 +1,29 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { AuthMenuButtonComponent } from './auth-menu-button/auth-menu-button.component';
import { AuthLoginComponent } from './auth-login/auth-login.component';
import { AuthRegisterComponent } from './auth-register/auth-register.component';
import { AuthMenuUserComponent } from './auth-menu-button/auth-menu-user/auth-menu-user.component';
import { ApiModule } from '../api/api.module';
const entryComponents = [AuthMenuUserComponent, AuthMenuButtonComponent, AuthLoginComponent, AuthRegisterComponent];
const components = [...entryComponents];
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ReactiveFormsModule,
ApiModule
],
entryComponents: entryComponents,
declarations: components,
exports: components,
providers: []
})
export class AuthModule {}

@ -0,0 +1,4 @@
export interface User {
email: string;
name: string;
}

@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { Router, CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot, UrlTree } from '@angular/router';
import { AuthService } from './auth.service';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {}
canActivate(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean
| UrlTree
| Observable<boolean
| UrlTree>
| Promise<boolean | UrlTree> {
if (!this.auth.currentUser$.value) {
this.router.navigateByUrl('/login');
}
return this.auth.currentUser$.value !== null;
}
}

@ -0,0 +1,59 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { User } from '../models/user.model';
import { ApiService } from 'src/app/api/api.service';
import { catchError, tap } from 'rxjs/operators';
const JWT_LOCALSTORE_KEY = 'jwt';
const USER_LOCALSTORE_KEY = 'user';
@Injectable({
providedIn: 'root'
})
export class AuthService {
currentUser$: BehaviorSubject<User> = new BehaviorSubject<User>(null);
constructor( private api: ApiService ) {
this.initToken();
}
initToken() {
const token = localStorage.getItem(JWT_LOCALSTORE_KEY);
const user = <User> JSON.parse(localStorage.getItem(USER_LOCALSTORE_KEY));
if (token && user) {
this.setTokenAndUser(token, user);
}
}
setTokenAndUser(token: string, user: User) {
localStorage.setItem(JWT_LOCALSTORE_KEY, token);
localStorage.setItem(USER_LOCALSTORE_KEY, JSON.stringify(user));
this.api.setAuthToken(token);
this.currentUser$.next(user);
}
async login(email: string, password: string): Promise<any> {
return this.api.post('/users/auth/login',
{email: email, password: password})
.then((res) => {
this.setTokenAndUser(res.token, res.user);
return res;
})
.catch((e) => { throw e; });
// return user !== undefined;
}
logout(): boolean {
this.setTokenAndUser(null, null);
return true;
}
register(user: User, password: string): Promise<any> {
return this.api.post('/users/auth/',
{email: user.email, password: password})
.then((res) => {
this.setTokenAndUser(res.token, res.user);
return res;
})
.catch((e) => { throw e; });
}
}

@ -0,0 +1,6 @@
<ion-card class="photo-card" *ngIf="feedItem">
<ion-img [src]="feedItem.url"></ion-img>
<ion-card-content>
<p>{{feedItem.caption}}</p>
</ion-card-content>
</ion-card>

@ -0,0 +1,14 @@
.photo-card{
max-width: 500px;
overflow: hidden;
background: var(--ion-color-primary-contrast);
margin: 30px 0px;
}
.photo-card ion-img {
max-height: 532px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}

@ -0,0 +1,42 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FeedItemComponent } from './feed-item.component';
import { feedItemMocks } from '../models/feed-item.model';
describe('FeedItemComponent', () => {
let component: FeedItemComponent;
let fixture: ComponentFixture<FeedItemComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FeedItemComponent ],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FeedItemComponent);
component = fixture.componentInstance;
component.feedItem = feedItemMocks[0];
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the image url to the feedItem', () => {
const app = fixture.nativeElement;
const img = app.querySelectorAll('ion-img');
expect(img.length).toEqual(1);
expect(img[0].src).toEqual(feedItemMocks[0].url);
});
it('should display the caption', () => {
const app = fixture.nativeElement;
const paragraphs = app.querySelectorAll('p');
expect(([].slice.call(paragraphs)).map((x) => x.innerText)).toContain(feedItemMocks[0].caption);
});
});

@ -0,0 +1,16 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { FeedItem } from '../models/feed-item.model';
@Component({
selector: 'app-feed-item',
templateUrl: './feed-item.component.html',
styleUrls: ['./feed-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FeedItemComponent implements OnInit {
@Input() feedItem: FeedItem;
constructor() { }
ngOnInit() {}
}

@ -0,0 +1,6 @@
<div class="feed">
<app-feed-item
*ngFor="let item of feedItems"
[feedItem]="item">
</app-feed-item>
</div>

@ -0,0 +1,7 @@
.feed {
display: flex;
flex-direction: column;
align-items: center;
background: var(--ion-color-light-tint);
}

@ -0,0 +1,30 @@
import { Component, OnInit, Input, OnDestroy } from '@angular/core';
import { FeedItem } from '../models/feed-item.model';
import { FeedProviderService } from '../services/feed.provider.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-feed-list',
templateUrl: './feed-list.component.html',
styleUrls: ['./feed-list.component.scss'],
})
export class FeedListComponent implements OnInit, OnDestroy {
@Input() feedItems: FeedItem[];
subscriptions: Subscription[] = [];
constructor( private feed: FeedProviderService ) { }
async ngOnInit() {
this.subscriptions.push(
this.feed.currentFeed$.subscribe((items) => {
this.feedItems = items;
}));
await this.feed.getFeed();
}
ngOnDestroy(): void {
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
}
}

@ -0,0 +1,7 @@
<ion-button
color="primary"
type="submit"
[disabled]="!isLoggedIn"
(click)="presentUploadForm($event)">
Create a New Post
</ion-button>

@ -0,0 +1,38 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { FeedUploadComponent } from '../feed-upload.component';
import { AuthService } from 'src/app/auth/services/auth.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-feed-upload-button',
templateUrl: './feed-upload-button.component.html',
styleUrls: ['./feed-upload-button.component.scss'],
})
export class FeedUploadButtonComponent implements OnInit, OnDestroy {
isLoggedIn: boolean;
loginSub: Subscription;
constructor(private modalController: ModalController, private auth: AuthService) { }
ngOnInit() {
this.auth.currentUser$.subscribe((user) => {
this.isLoggedIn = user !== null;
});
}
ngOnDestroy(): void {
if (this.loginSub) {
this.loginSub.unsubscribe();
}
}
async presentUploadForm(ev: any) {
const modal = await this.modalController.create({
component: FeedUploadComponent,
});
return await modal.present();
}
}

@ -0,0 +1,19 @@
<form [formGroup]="uploadForm" (submit)="onSubmit($event)">`
<label>
<input type="file" (change)="selectImage($event)" accept="image/*" style="display:none"/>
<img *ngIf="previewDataUrl" [src]="previewDataUrl" style="width:50px; height: 50px;" />
<a *ngIf="!previewDataUrl" ion-button color="secondary">
Select a Photo
</a>
</label>
<ion-item>
<ion-label position="floating" color="primary">Caption</ion-label>
<ion-input type="text" formControlName="caption" required></ion-input>
</ion-item>
<ion-button
color="primary"
type="submit"
[disabled]="!uploadForm.valid || !file">
Post
</ion-button>
</form>

@ -0,0 +1,67 @@
import { Component, OnInit } from '@angular/core';
import { Validators, FormBuilder, FormGroup, FormControl } from '@angular/forms';
import { FeedProviderService } from '../services/feed.provider.service';
import { LoadingController, ModalController } from '@ionic/angular';
@Component({
selector: 'app-feed-upload',
templateUrl: './feed-upload.component.html',
styleUrls: ['./feed-upload.component.scss'],
})
export class FeedUploadComponent implements OnInit {
previewDataUrl;
file: File;
uploadForm: FormGroup;
constructor(
private feed: FeedProviderService,
private formBuilder: FormBuilder,
private loadingController: LoadingController,
private modalController: ModalController
) { }
ngOnInit() {
this.uploadForm = this.formBuilder.group({
caption: new FormControl('', Validators.required)
});
}
setPreviewDataUrl(file: Blob) {
const reader = new FileReader();
reader.onloadend = () => {
this.previewDataUrl = reader.result;
};
reader.readAsDataURL(file);
}
selectImage(event) {
const file = event.srcElement.files;
if (!file) {
return;
}
this.file = file[0];
this.setPreviewDataUrl(this.file);
}
onSubmit($event) {
$event.preventDefault();
this.loadingController.create();
if (!this.uploadForm.valid || !this.file) { return; }
this.feed.uploadFeedItem(this.uploadForm.controls.caption.value, this.file)
.then((result) => {
this.modalController.dismiss();
this.loadingController.dismiss();
});
}
cancel() {
this.modalController.dismiss();
}
}

@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { FeedListComponent } from './feed-list/feed-list.component';
import { FeedItemComponent } from './feed-item/feed-item.component';
import { FeedUploadComponent } from './feed-upload/feed-upload.component';
import { FeedUploadButtonComponent } from './feed-upload/feed-upload-button/feed-upload-button.component';
import { FeedProviderService } from './services/feed.provider.service';
const entryComponents = [FeedUploadComponent];
const components = [FeedListComponent, FeedItemComponent, FeedUploadComponent, FeedUploadButtonComponent];
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ReactiveFormsModule
],
declarations: components,
exports: components,
entryComponents: entryComponents,
providers: [FeedProviderService]
})
export class FeedModule {}

@ -0,0 +1,23 @@
export interface FeedItem {
id: number;
url: string;
caption: string;
}
export const feedItemMocks: FeedItem[] = [
{
id: 0,
url: '/assets/mock/xander0.jpg',
caption: 'Such a cute pup'
},
{
id: 0,
url: '/assets/mock/xander1.jpg',
caption: 'Who\'s a good boy?'
},
{
id: 0,
url: '/assets/mock/xander2.jpg',
caption: 'Majestic.'
}
];

@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { FeedItem, feedItemMocks } from '../models/feed-item.model';
import { BehaviorSubject } from 'rxjs';
import { ApiService } from '../../api/api.service';
@Injectable({
providedIn: 'root'
})
export class FeedProviderService {
currentFeed$: BehaviorSubject<FeedItem[]> = new BehaviorSubject<FeedItem[]>([]);
constructor(private api: ApiService) { }
async getFeed(): Promise<BehaviorSubject<FeedItem[]>> {
const req = await this.api.get('/feed');
const items = <FeedItem[]> req.rows;
this.currentFeed$.next(items);
return Promise.resolve(this.currentFeed$);
}
async uploadFeedItem(caption: string, file: File): Promise<any> {
const res = await this.api.upload('/feed', file, {caption: caption, url: file.name});
const feed = [res, ...this.currentFeed$.value];
this.currentFeed$.next(feed);
return res;
}
}

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { RouterModule } from '@angular/router';
import { FeedModule } from '../feed/feed.module';
import { HomePage } from './home.page';
@NgModule({
imports: [
FeedModule,
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild([
{
path: '',
component: HomePage
}
])
],
declarations: [HomePage]
})
export class HomePageModule {}

@ -0,0 +1,4 @@
<ion-content>
<app-feed-upload-button></app-feed-upload-button>
<app-feed-list></app-feed-list>
</ion-content>

@ -0,0 +1,28 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomePage } from './home.page';
describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomePage ],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
appName = environment.appName;
}

@ -0,0 +1,13 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>
{{ appName }}
</ion-title>
<ion-buttons slot="end">
<app-auth-menu-button></app-auth-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

@ -0,0 +1,5 @@
ion-title {
font-weight: bold;
font-family: 'Dancing Script', cursive;
font-size: 180%;
}

@ -0,0 +1,36 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MenubarComponent } from './menubar.component';
import { environment } from '../../environments/environment';
describe('MenubarPage', () => {
let component: MenubarComponent;
let fixture: ComponentFixture<MenubarComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MenubarComponent ],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MenubarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('title should be enviornment.AppTitle', () => {
const app = fixture.nativeElement;
const title = app.querySelectorAll('ion-title');
expect(title.length).toEqual(1);
expect(title[0].innerText).toEqual(environment.appName);
});
});

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-menubar',
templateUrl: './menubar.component.html',
styleUrls: ['./menubar.component.scss'],
})
export class MenubarComponent implements OnInit {
public appName = environment.appName;
constructor() { }
ngOnInit() {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

@ -0,0 +1 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,18 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: true,
appName: "Udagram",
apiHost: "http://localhost:8080/api/v0",
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

@ -0,0 +1,18 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
appName: 'Udagram',
apiHost: 'http://localhost:8080/api/v0'
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

@ -0,0 +1,14 @@
// http://ionicframework.com/docs/theming/
@import '~@ionic/angular/css/core.css';
@import '~@ionic/angular/css/normalize.css';
@import '~@ionic/angular/css/structure.css';
@import '~@ionic/angular/css/typography.css';
@import '~@ionic/angular/css/display.css';
@import '~@ionic/angular/css/padding.css';
@import '~@ionic/angular/css/float-elements.css';
@import '~@ionic/angular/css/text-alignment.css';
@import '~@ionic/angular/css/text-transformation.css';
@import '~@ionic/angular/css/flex-utils.css';
// Fancy Fonts
@import url('https://fonts.googleapis.com/css?family=Dancing+Script:700');

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
<base href="/" />
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<app-root></app-root>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save