├── .angular-cli.json ├── .editorconfig ├── .firebaserc ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── README.md ├── database.rules.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.json ├── firebase.json ├── functions ├── index.js └── package.json ├── karma.conf.js ├── npm-debug.log.2957979853 ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── account │ │ ├── account.module.ts │ │ ├── profile │ │ │ ├── profile-page.component.html │ │ │ └── profile-page.component.ts │ │ ├── routing │ │ │ └── accountRouting.module.ts │ │ ├── shared │ │ │ ├── accountShared.module.ts │ │ │ └── profilePicture │ │ │ │ └── profilePicture.component.ts │ │ ├── state │ │ │ ├── accountState.module.ts │ │ │ └── store.config.ts │ │ ├── todos │ │ │ ├── state │ │ │ │ ├── todos.actionTypes.ts │ │ │ │ ├── todos.actions.ts │ │ │ │ ├── todos.effects.ts │ │ │ │ ├── todos.reducer.spec.ts │ │ │ │ ├── todos.reducer.ts │ │ │ │ └── todos.state.ts │ │ │ ├── todo-display │ │ │ │ ├── todo-display.component.html │ │ │ │ ├── todo-display.component.spec.ts │ │ │ │ └── todo-display.component.ts │ │ │ ├── todo-edit │ │ │ │ ├── todo-edit.component.html │ │ │ │ ├── todo-edit.component.spec.ts │ │ │ │ └── todo-edit.component.ts │ │ │ ├── todo.ts │ │ │ ├── todo │ │ │ │ ├── todo.component.html │ │ │ │ └── todo.component.ts │ │ │ ├── todos.module.ts │ │ │ └── todos.service.ts │ │ └── user │ │ │ ├── infoPage │ │ │ ├── infoPage.component.html │ │ │ └── infoPage.component.ts │ │ │ ├── send-email-verification │ │ │ ├── form │ │ │ │ ├── sendEmailVerification.component.html │ │ │ │ └── sendEmailVerification.component.ts │ │ │ ├── sendEmailVerification.module.ts │ │ │ └── state │ │ │ │ ├── initialState.ts │ │ │ │ ├── resendEmailVerification.actionTypes.ts │ │ │ │ ├── resendEmailVerification.actions.ts │ │ │ │ ├── resendEmailVerification.effects.ts │ │ │ │ ├── resendEmailVerification.reducer.ts │ │ │ │ └── resendEmailVerificationState.ts │ │ │ ├── state │ │ │ ├── store.config.ts │ │ │ └── userState.module.ts │ │ │ ├── update-email │ │ │ ├── form │ │ │ │ ├── updateEmail.component.html │ │ │ │ └── updateEmail.component.ts │ │ │ ├── state │ │ │ │ ├── initialState.ts │ │ │ │ ├── updateEmail.actionTypes.ts │ │ │ │ ├── updateEmail.actions.ts │ │ │ │ ├── updateEmail.effects.spec.ts │ │ │ │ ├── updateEmail.effects.ts │ │ │ │ ├── updateEmail.reducer.spec.ts │ │ │ │ ├── updateEmail.reducer.ts │ │ │ │ └── updateEmailState.ts │ │ │ └── updateEmail.module.ts │ │ │ ├── update-password │ │ │ ├── form │ │ │ │ ├── updatePassword.component.html │ │ │ │ └── updatePassword.component.ts │ │ │ ├── state │ │ │ │ ├── initialState.ts │ │ │ │ ├── updatePassword.actionTypes.ts │ │ │ │ ├── updatePassword.actions.ts │ │ │ │ ├── updatePassword.effects.ts │ │ │ │ ├── updatePassword.reducer.spec.ts │ │ │ │ ├── updatePassword.reducer.ts │ │ │ │ └── updatePasswordState.ts │ │ │ └── updatePassword.module.ts │ │ │ ├── update-photo-url │ │ │ ├── form │ │ │ │ ├── updatePhoto-url.component.html │ │ │ │ └── updatePhoto-url.component.ts │ │ │ ├── state │ │ │ │ ├── initialState.ts │ │ │ │ ├── updatePhotoUrl.actionTypes.ts │ │ │ │ ├── updatePhotoUrl.actions.ts │ │ │ │ ├── updatePhotoUrl.effects.ts │ │ │ │ ├── updatePhotoUrl.reducer.spec.ts │ │ │ │ ├── updatePhotoUrl.reducer.ts │ │ │ │ └── updatePhotoUrlState.ts │ │ │ └── updatePhotoUrl.module.ts │ │ │ └── user.module.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── error-handler │ │ └── custom-error-handler.ts │ ├── firebase │ │ ├── firebase.config.ts │ │ └── index.ts │ ├── footer │ │ ├── footer.component.html │ │ ├── footer.component.spec.ts │ │ └── footer.component.ts │ ├── helpers │ │ ├── actionMap.ts │ │ ├── assign.spec.ts │ │ ├── assign.ts │ │ ├── assignDeep.spec.ts │ │ ├── assignDeep.ts │ │ ├── getErrorMessage.spec.ts │ │ ├── getErrorMessage.ts │ │ ├── hashReducer.ts │ │ ├── index.ts │ │ ├── subscriber.component.ts │ │ └── useDefaultState.ts │ ├── landing-page │ │ ├── landing-page.component.html │ │ ├── landing-page.component.spec.ts │ │ └── landing-page.component.ts │ ├── log-in │ │ ├── log-in.component.html │ │ └── log-in.component.ts │ ├── nav │ │ ├── nav.component.html │ │ └── nav.component.ts │ ├── reset-password │ │ ├── resetPassword.component.html │ │ ├── resetPassword.component.ts │ │ ├── resetPassword.module.ts │ │ └── state │ │ │ ├── form │ │ │ ├── resetPasswordForm.actionTypes.ts │ │ │ ├── resetPasswordForm.actions.ts │ │ │ └── resetPasswordForm.reducer.ts │ │ │ ├── resetPassword.effects.ts │ │ │ ├── resetPasswordState.module.ts │ │ │ └── store.config.ts │ ├── resources │ │ └── messages.ts │ ├── routing │ │ └── appRouting.module.ts │ ├── shared │ │ ├── forms │ │ │ ├── showErrors.ts │ │ │ ├── typedAbstractControl.ts │ │ │ ├── typedFormArray.ts │ │ │ ├── typedFormControl.ts │ │ │ ├── typedFormGroup.ts │ │ │ └── typedValidatorFn.ts │ │ ├── google │ │ │ └── google-plus-auth-button.ts │ │ ├── guards │ │ │ ├── isLoggedIn.guard.ts │ │ │ └── isNotLoggedIn.guard.ts │ │ ├── modal │ │ │ ├── modal.component.html │ │ │ ├── modal.component.spec.ts │ │ │ └── modal.component.ts │ │ ├── panel │ │ │ ├── panel.component.html │ │ │ └── panel.component.ts │ │ ├── shared.module.ts │ │ ├── submit-button │ │ │ ├── submit-button.component.html │ │ │ └── submit-button.component.ts │ │ ├── utils │ │ │ └── scrollToElement.ts │ │ └── validation-message │ │ │ └── validation-message.component.ts │ ├── sign-up │ │ ├── signUp.component.html │ │ ├── signUp.component.ts │ │ ├── signUp.module.ts │ │ └── state │ │ │ ├── form │ │ │ ├── signUpForm.actionTypes.ts │ │ │ ├── signUpForm.actions.ts │ │ │ ├── signUpForm.effects.ts │ │ │ └── signUpForm.reducer.ts │ │ │ ├── signUp.state.module.ts │ │ │ └── store.config.ts │ ├── store │ │ ├── app.state.ts │ │ ├── forms │ │ │ ├── form.reducer.factory.spec.ts │ │ │ ├── form.reducer.factory.ts │ │ │ └── formState.ts │ │ ├── global │ │ │ ├── global.actionTypes.ts │ │ │ ├── global.actions.ts │ │ │ ├── global.reducer.spec.ts │ │ │ └── global.reducer.ts │ │ ├── nav │ │ │ ├── nav.actionTypes.ts │ │ │ ├── nav.actions.ts │ │ │ ├── nav.reducer.spec.ts │ │ │ ├── nav.reducer.ts │ │ │ └── nav.state.ts │ │ ├── state.module.ts │ │ ├── store.config.ts │ │ ├── testing │ │ │ ├── actions.spec.ts │ │ │ ├── index.ts │ │ │ └── reducerTestHelpers.ts │ │ ├── user │ │ │ ├── globalUserState.module.ts │ │ │ ├── logIn │ │ │ │ ├── logIn.actionTypes.ts │ │ │ │ ├── logIn.actions.ts │ │ │ │ ├── logIn.effects.ts │ │ │ │ ├── logIn.reducer.spec.ts │ │ │ │ ├── logIn.reducer.ts │ │ │ │ └── login.effects.spec.ts │ │ │ ├── store.config.ts │ │ │ └── user.state.ts │ │ └── utils │ │ │ └── featureState.ts │ └── validators │ │ ├── emailValid.ts │ │ ├── index.ts │ │ ├── minLength.ts │ │ ├── passwordValid.ts │ │ ├── validUrl.ts │ │ ├── validator.d.ts │ │ └── valuesEqual.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.d.ts │ ├── environment.dev.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.json └── vendors.ts ├── tslint.json └── yarn.lock /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "firebase" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.json", 19 | "prefix": "app", 20 | "styles": [ 21 | "styles.css" 22 | ], 23 | "scripts": [], 24 | "environmentSource": "environments/environment.ts", 25 | "environments": { 26 | "dev": "environments/environment.dev.ts", 27 | "prod": "environments/environment.prod.ts" 28 | } 29 | } 30 | ], 31 | "e2e": { 32 | "protractor": { 33 | "config": "./protractor.conf.js" 34 | } 35 | }, 36 | "lint": [ 37 | { 38 | "files": "src/**/*.ts", 39 | "project": "src/tsconfig.json" 40 | }, 41 | { 42 | "files": "e2e/**/*.ts", 43 | "project": "e2e/tsconfig.json" 44 | } 45 | ], 46 | "test": { 47 | "karma": { 48 | "config": "./karma.conf.js" 49 | } 50 | }, 51 | "defaults": { 52 | "styleExt": "css", 53 | "component": {} 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "adviewer-73e3f" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /functions/node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | #System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "es5" 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | cache: yarn 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 4, 4 | "typescript.tsdk": "./node_modules/typescript/lib", 5 | "html.format.wrapAttributes": "force-aligned", 6 | "html.format.indentInnerHtml": true 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/bave8672/angular-firebase-starter.svg?branch=master)](https://travis-ci.org/bave8672/angular-firebase-starter) 2 | 3 | # Angular 2 Firebase App 4 | 5 | An app boilerplate using: 6 | 7 | - [Angular 5](https://github.com/search?q=topic%3Aangular+org%3Aangular&type=Repositories) 8 | - [Angular CLI](https://github.com/angular/angular-cli) 9 | - [Firebase (angularfire2 v5)](https://github.com/angular/angularfire2) 10 | - [Ngrx Store](https://github.com/ngrx/store) and [Ngrx Effects](https://github.com/ngrx/effects) 11 | 12 | Featuring: 13 | 14 | - A full authentication pipeline including both Google and email authentication 15 | - User accounts 16 | - Business logic (using todos as an example) 17 | 18 | *[View the demo](https://adviewer-73e3f.firebaseapp.com/)* 19 | 20 | ## Motivation 21 | 22 | The purpose of this boilerplate is to provide a more advanced starting point for angular 2 apps that already features a well defined workflow, state management and design layout. 23 | 24 | The template is designed to be easy to build upon and extend to suit your app. 25 | 26 | ## Quickstart 27 | 28 | ```bash 29 | # Clone the repo 30 | git clone https://github.com/bave8672/angular-firebase-starter.git 31 | 32 | # Change directory to repo 33 | cd angular-firebase-starter 34 | 35 | # Use npm or yarn to install the dependencies: 36 | npm install 37 | 38 | # OR 39 | yarn 40 | 41 | # Start the server 42 | npm run start 43 | ``` 44 | 45 | Navigate to [http://localhost:4200/](http://localhost:4200/) in your browser 46 | 47 | ## Highlights 48 | 49 | The following sections briefly cover the most important features of this starter app. 50 | 51 | ### Ngrx integration 52 | 53 | All app state is managed via the redux pattern, using the [Ngrx Store](https://github.com/ngrx/store) implementation. Components allow the UI to fire actions, and contain minial business logic. 54 | 55 | The store is architected similarly to the ngrx [example app](https://github.com/ngrx/example-app), with the global reducer composed from several sub-reducers to manage separate concerns. 56 | 57 | [Effects](https://github.com/ngrx/effects) are used to handle asynchronous state events, such as interacting with the firebase auth and db services. 58 | 59 | ### Firebase authentication 60 | 61 | The app supports firebase auth (via both Google OAuth and email/password) out of the box. 62 | 63 | In the case of email auth, the app allows the user to update their password, profile picture and resend their verification email. 64 | 65 | Adding other auth providers is straightforward: See the [firebase docs](https://firebase.google.com/docs/auth/web/start); 66 | 67 | ### Forms 68 | 69 | Forms are always an important aspect of web applications. This starter attempts to standardise form interactions beyond what angular provides by: 70 | 71 | - Exclusively using Angular's [reactive forms](https://angular.io/docs/ts/latest/guide/reactive-forms.html) module. 72 | - Requiring all components to inherit from a base [FormComponent](./src/app/helpers/form.component.ts) 73 | - Using an underlying [FormState](./src/app/store/formState.ts) to capture the state of every form (e.g whether a request is in-flight, whether to display an error message...) in a redux-friendly way. 74 | - Providing a shared [validation message component](./src/app/shared/validation-message/validation-message.component.ts) to display validation warnings and messages. 75 | - Providing various validation functions and helpers to e.g. parse errors from http results. 76 | 77 | ### Styles 78 | 79 | [Bootstrap](http://getbootstrap.com/) CSS is included by default. To promote customisability, no custom css has been added to the boilerplate, and JQuery and bootstrap's Javascript packages have been excluded. 80 | 81 | If you want to use something else like Foundation instead, simply remove the bootstrap package and update the styles.css to import your library of choice, and replace the markup classes with your own. 82 | 83 | Since the project uses the CLI, you can also add your own CSS or SASS to components without any extra steps. 84 | 85 | ### Builds, environments and deployment 86 | 87 | The project is set up to take advantage of firebase hosting. 88 | 89 | Add your project's firebase config to [firebase.config.ts](.src/app/firebase/firebase.config.ts), the run 90 | 91 | ```bash 92 | npm run build-deploy 93 | ``` 94 | 95 | to build the app in production mode and deploy to it to the firebase server. 96 | 97 | You can add different firebase configurations for different environments - see the environment files. 98 | 99 | In production mode the app's [custom error handler](./src/app/error-handler/custom-error-handler.ts) will post errors to your firebase db. 100 | 101 | ### Testing 102 | 103 | ```bash 104 | # Run unit tests with coverage in PhantomJS 105 | npm run test 106 | 107 | # Debug unit tests in Chrome 108 | npm run test:debug 109 | ``` 110 | 111 | Planned: 112 | 113 | - Better test coverage (currently ~70%) 114 | - E2E Tests 115 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "todos": { 4 | "$uid": { 5 | ".read": "auth != null && auth.uid === $uid", 6 | ".write": "auth != null && auth.uid === $uid" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { FirebasePage } from './app.po'; 2 | 3 | describe('firebase App', () => { 4 | let page: FirebasePage; 5 | 6 | beforeEach(() => { 7 | page = new FirebasePage(); 8 | }); 9 | 10 | it('Should display the title header WHEN on the landing page', () => { 11 | page.navigateTo('/'); 12 | expect(page.text('title')).toEqual('Hello, world!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class FirebasePage { 4 | 5 | navigateTo(path: string) { 6 | return browser.get(path); 7 | } 8 | 9 | element(name: string) { 10 | return element(by.css(`[data-e2e="${name}"]`)); 11 | } 12 | 13 | text(name: string) { 14 | return this.element(name).getText(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "es2016" 9 | ], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "outDir": "../dist/out-tsc-e2e", 13 | "sourceMap": true, 14 | "target": "es6", 15 | "typeRoots": [ 16 | "../node_modules/@types" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "dist", 7 | "rewrites": [ 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | var functions = require('firebase-functions'); 2 | 3 | // // Start writing Firebase Functions 4 | // // https://firebase.google.com/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }) 9 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "^4.1.2", 6 | "firebase-functions": "^0.5" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-phantomjs-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('@angular/cli/plugins/karma') 15 | ], 16 | client:{ 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | files: [ 20 | { pattern: './src/test.ts', watched: false } 21 | ], 22 | preprocessors: { 23 | './src/test.ts': ['@angular/cli'] 24 | }, 25 | mime: { 26 | 'text/x-typescript': ['ts','tsx'] 27 | }, 28 | coverageIstanbulReporter: { 29 | reports: [ 'html', 'lcovonly', 'text-summary'], 30 | fixWebpackSourcePaths: true 31 | }, 32 | angularCli: { 33 | config: './.angular-cli.json', 34 | environment: 'dev' 35 | }, 36 | reporters: config.angularCli && config.angularCli.codeCoverage 37 | ? ['progress', 'coverage-istanbul'] 38 | : ['progress', 'kjhtml'], 39 | port: 9876, 40 | colors: true, 41 | logLevel: config.LOG_INFO, 42 | autoWatch: true, 43 | browsers: ['PhantomJS'], 44 | singleRun: false 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /npm-debug.log.2957979853: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bave8672/angular-firebase-starter/3cb4873c40d58964ce79d1d9bf400659f1154207/npm-debug.log.2957979853 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve --environment=dev", 9 | "test": "ng test --browsers=PhantomJS --single-run --code-coverage", 10 | "test:debug": "ng test --browsers=Chrome --no-single-run", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e", 13 | "build": "npm run build:prod", 14 | "build:prod": "ng build --prod --aot", 15 | "deploy": "firebase deploy", 16 | "build-deploy": "npm run build && npm run deploy" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/common": "^5.0.0", 21 | "@angular/compiler": "^5.0.0", 22 | "@angular/core": "^5.0.0", 23 | "@angular/forms": "^5.0.0", 24 | "@angular/http": "^5.0.0", 25 | "@angular/platform-browser": "^5.0.0", 26 | "@angular/platform-browser-dynamic": "^5.0.0", 27 | "@angular/router": "^5.0.0", 28 | "@ngrx/effects": "^4.0.0", 29 | "@ngrx/router-store": "^4.0.0", 30 | "@ngrx/store": "^4.0.0", 31 | "angularfire2": "^5.0.0-rc.6", 32 | "bootstrap": "^3.3.7", 33 | "bootstrap-social": "^5.1.1", 34 | "core-js": "^2.4.1", 35 | "firebase": "latest", 36 | "font-awesome": "^4.7.0", 37 | "ngrx-store-localstorage": "^0.1.5", 38 | "rxjs": "latest", 39 | "zone.js": "^0.7.6" 40 | }, 41 | "devDependencies": { 42 | "@angular/cli": "latest", 43 | "@angular/compiler-cli": "latest", 44 | "@types/jasmine": "2.5.38", 45 | "@types/node": "~6.0.60", 46 | "codelyzer": "~2.0.0-beta.4", 47 | "firebase-tools": "^3.5.0", 48 | "jasmine-core": "~2.5.2", 49 | "jasmine-spec-reporter": "~3.2.0", 50 | "karma": "~1.4.1", 51 | "karma-chrome-launcher": "~2.0.0", 52 | "karma-cli": "~1.0.1", 53 | "karma-coverage-istanbul-reporter": "^0.2.0", 54 | "karma-jasmine": "~1.1.0", 55 | "karma-jasmine-html-reporter": "^0.2.2", 56 | "karma-phantomjs-launcher": "^1.0.4", 57 | "prettier": "^1.10.2", 58 | "protractor": "~5.1.0", 59 | "ts-node": "~2.0.0", 60 | "tslint": "~4.4.2", 61 | "typescript": ">=2.4.2 <2.7.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | beforeLaunch: function() { 24 | require('ts-node').register({ 25 | project: 'e2e' 26 | }); 27 | }, 28 | onPrepare() { 29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { SharedModule } from '../shared/shared.module'; 2 | import { ProfilePageComponent } from './profile/profile-page.component'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { AccountRoutingModule } from 'app/account/routing/accountRouting.module'; 6 | import { TodosModule } from 'app/account/todos/todos.module'; 7 | import { AccountSharedModule } from 'app/account/shared/accountShared.module'; 8 | import { AccountStateModule } from 'app/account/state/accountState.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | AccountSharedModule, 13 | AccountStateModule, 14 | TodosModule, 15 | AccountRoutingModule, 16 | ], 17 | declarations: [ProfilePageComponent], 18 | }) 19 | export class AccountModule {} 20 | -------------------------------------------------------------------------------- /src/app/account/profile/profile-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Your Todo List

3 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/account/profile/profile-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; 2 | import { TodosService } from 'app/account/todos/todos.service'; 3 | 4 | @Component({ 5 | templateUrl: './profile-page.component.html', 6 | changeDetection: ChangeDetectionStrategy.OnPush 7 | }) 8 | export class ProfilePageComponent { 9 | 10 | todos$ = this.todosService.todos().valueChanges(); 11 | 12 | constructor(private todosService: TodosService) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/app/account/routing/accountRouting.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { ProfilePageComponent } from 'app/account/profile/profile-page.component'; 5 | import { IsLoggedInGuard } from 'app/shared/guards/isLoggedIn.guard'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forChild([ 10 | { path: '', component: ProfilePageComponent }, 11 | { path: 'profile', component: ProfilePageComponent }, 12 | { path: 'info', loadChildren: '../user/user.module#UserModule' }, 13 | { path: '**', component: ProfilePageComponent }, 14 | ]), 15 | ], 16 | exports: [RouterModule], 17 | }) 18 | export class AccountRoutingModule {} 19 | -------------------------------------------------------------------------------- /src/app/account/shared/accountShared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ProfilePictureComponent } from 'app/account/shared/profilePicture/profilePicture.component'; 3 | import { SharedModule } from 'app/shared/shared.module'; 4 | 5 | @NgModule({ 6 | imports: [SharedModule], 7 | declarations: [ProfilePictureComponent], 8 | exports: [SharedModule, ProfilePictureComponent], 9 | }) 10 | export class AccountSharedModule {} 11 | -------------------------------------------------------------------------------- /src/app/account/shared/profilePicture/profilePicture.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { AngularFireAuth } from 'angularfire2/auth'; 3 | 4 | export const DEFAULT_PHOTO_URL = 5 | 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'; 6 | 7 | @Component({ 8 | selector: 'app-profile-picture', 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | template: `Profile Picture`, 11 | styles: ['img { height: 3em; width: 3em; }'], 12 | }) 13 | export class ProfilePictureComponent { 14 | @Input() src: string | undefined; 15 | photoUrl$ = this.auth.authState 16 | .filter(a => !!a) 17 | .map(a => a.photoURL || DEFAULT_PHOTO_URL); 18 | 19 | constructor(protected auth: AngularFireAuth) {} 20 | } 21 | -------------------------------------------------------------------------------- /src/app/account/state/accountState.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { StoreModule } from '@ngrx/store'; 3 | import { 4 | accountReducers, 5 | ACCOUNT_FEATURE_KEY, 6 | initialAccountState, 7 | } from 'app/account/state/store.config'; 8 | import { EffectsModule } from '@ngrx/effects'; 9 | import { TodosEffects } from 'app/account/todos/state/todos.effects'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | StoreModule.forFeature(ACCOUNT_FEATURE_KEY, accountReducers, { 14 | initialState: initialAccountState, 15 | }), 16 | EffectsModule.forFeature([TodosEffects]), 17 | ], 18 | }) 19 | export class AccountStateModule {} 20 | -------------------------------------------------------------------------------- /src/app/account/state/store.config.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap } from '@ngrx/store'; 2 | import { AppState } from 'app/store/app.state'; 3 | import { AppFeatureState } from 'app/store/utils/featureState'; 4 | import { 5 | TodosState, 6 | initialTodosState, 7 | } from 'app/account/todos/state/todos.state'; 8 | import { todosReducer } from 'app/account/todos/state/todos.reducer'; 9 | 10 | export const ACCOUNT_FEATURE_KEY = 'account'; 11 | 12 | export interface AccountFeatureState { 13 | todos: TodosState; 14 | } 15 | 16 | export type AccountAppState = AppFeatureState< 17 | AppState, 18 | typeof ACCOUNT_FEATURE_KEY, 19 | AccountFeatureState 20 | >; 21 | 22 | export const accountReducers: ActionReducerMap = { 23 | todos: todosReducer, 24 | }; 25 | 26 | export const initialAccountState: AccountFeatureState = { 27 | todos: initialTodosState, 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/account/todos/state/todos.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace TodosActionTypes { 2 | export const Edit = '[Todos] Edit'; 3 | export const CloseEdit = '[Todos] Close Edit'; 4 | export const Update = '[Todos] Update'; 5 | export const Remove = '[Todos] Remove'; 6 | export const Delete = '[Todos] Delete'; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/account/todos/state/todos.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { TodosActionTypes } from './todos.actionTypes'; 4 | import { Todo } from 'app/account/todos/todo'; 5 | 6 | export namespace TodosActions { 7 | 8 | export class Edit implements Action { 9 | readonly type = TodosActionTypes.Edit; 10 | /** 11 | * payload: uid 12 | */ 13 | constructor(public readonly payload: string | null) { } 14 | } 15 | 16 | export class CloseEdit implements Action { 17 | readonly type = TodosActionTypes.CloseEdit; 18 | readonly payload: void; 19 | } 20 | 21 | export class Update implements Action { 22 | readonly type = TodosActionTypes.Update; 23 | /** 24 | * payload: name 25 | */ 26 | constructor(public readonly payload: Todo) { } 27 | } 28 | 29 | export class Delete implements Action { 30 | readonly type = TodosActionTypes.Delete; 31 | /** 32 | * payload: uid 33 | */ 34 | constructor(public readonly payload: string) { } 35 | } 36 | 37 | export type TodosAction = 38 | Edit 39 | | CloseEdit 40 | | Update 41 | | Delete; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/account/todos/state/todos.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Effect } from '@ngrx/effects'; 3 | 4 | import { TodosService } from '../../todos/todos.service'; 5 | import { TodosActions } from './todos.actions'; 6 | import { TodosActionTypes } from './todos.actionTypes'; 7 | import { Store } from '@ngrx/store'; 8 | import { AccountAppState } from 'app/account/state/store.config'; 9 | import { Actions } from '@ngrx/effects'; 10 | 11 | @Injectable() 12 | export class TodosEffects { 13 | @Effect({ dispatch: false }) 14 | update$ = this.actions$ 15 | .ofType(TodosActionTypes.Update) 16 | .map((action: TodosActions.Update) => action.payload) 17 | .switchMap(todo => { 18 | if (!todo.uid) { 19 | const ref = this.todosService.todos().push({ 20 | name: todo.name, 21 | uid: '', 22 | }); 23 | return ref.set({ 24 | name: todo.name, 25 | uid: ref.key, 26 | }); 27 | } else { 28 | return this.todosService.todo(todo.uid).set(todo); 29 | } 30 | }); 31 | 32 | @Effect({ dispatch: false }) 33 | delete$ = this.actions$ 34 | .ofType(TodosActionTypes.Delete) 35 | .map((action: TodosActions.Delete) => action.payload) 36 | .map(uid => this.todosService.todo(uid).remove()); 37 | 38 | constructor( 39 | private actions$: Actions, 40 | private state: Store, 41 | private todosService: TodosService 42 | ) {} 43 | } 44 | -------------------------------------------------------------------------------- /src/app/account/todos/state/todos.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { initialTodosState, TodosState } from './todos.state'; 2 | import { shouldNotAlterStateOnUnknownAction } from 'app/store/testing'; 3 | import { assignDeep } from 'app/helpers'; 4 | import { todosReducer } from 'app/account/todos/state/todos.reducer'; 5 | import { TodosActions } from 'app/account/todos/state/todos.actions'; 6 | 7 | describe('Todos Reducer', () => { 8 | shouldNotAlterStateOnUnknownAction(todosReducer); 9 | 10 | let oldState: TodosState; 11 | 12 | beforeEach(() => { 13 | oldState = assignDeep(initialTodosState); 14 | }); 15 | 16 | it(`Assigns the uid of the edited todo to the editing prop 17 | WHEN edit is called`, () => { 18 | const newState = todosReducer(oldState, new TodosActions.Edit('123')); 19 | expect(newState.editing).toBe('123'); 20 | }); 21 | 22 | it(`Assigns an empty string to the editing prop 23 | WHEN Update is called`, () => { 24 | oldState.editing = '123'; 25 | const newState = todosReducer(oldState, new TodosActions.Update(null)); 26 | expect(newState.editing).toBe(''); 27 | }); 28 | 29 | it(`Assigns an empty string to the editing prop 30 | WHEN Close Edit is called`, () => { 31 | oldState.editing = '123'; 32 | const newState = todosReducer(oldState, new TodosActions.CloseEdit()); 33 | expect(newState.editing).toBe(''); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/account/todos/state/todos.reducer.ts: -------------------------------------------------------------------------------- 1 | import { TodosActionTypes } from './todos.actionTypes'; 2 | import { initialTodosState, TodosState } from './todos.state'; 3 | import { assign } from 'app/helpers'; 4 | import { TodosActions } from 'app/account/todos/state/todos.actions'; 5 | 6 | export function todosReducer( 7 | state: TodosState, 8 | action: TodosActions.TodosAction 9 | ): TodosState { 10 | switch (action.type) { 11 | case TodosActionTypes.Edit: 12 | return { ...state, editing: action.payload }; 13 | 14 | case TodosActionTypes.CloseEdit: 15 | case TodosActionTypes.Update: 16 | return { ...state, editing: '' }; 17 | 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/account/todos/state/todos.state.ts: -------------------------------------------------------------------------------- 1 | export interface TodosState { 2 | editing: string; // uid 3 | } 4 | 5 | export const initialTodosState: TodosState = { 6 | editing: '' 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/account/todos/todo-display/todo-display.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 |
9 | -------------------------------------------------------------------------------- /src/app/account/todos/todo-display/todo-display.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { Todo } from '../todo'; 2 | // import { Store } from '@ngrx/store'; 3 | // import { AppState, DefaultAppState, TodosActions } from '../../store'; 4 | // import { DisplayTodoComponent } from './todo-display.component'; 5 | // import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 6 | // import { Observable } from 'rxjs/Observable'; 7 | // import { assignDeep } from '../../helpers'; 8 | 9 | // describe('todo-display', () => { 10 | // let component: DisplayTodoComponent; 11 | // let fixture: ComponentFixture; 12 | // let todo: Todo; 13 | 14 | // class MockStateService { 15 | // dispatch() {} 16 | // } 17 | 18 | // beforeEach(async(() => { 19 | // TestBed.configureTestingModule({ 20 | // declarations: [DisplayTodoComponent], 21 | // providers: [ 22 | // { provide: StateService, useClass: MockStateService } 23 | // ] 24 | // }) 25 | // .compileComponents(); 26 | // })); 27 | 28 | // beforeEach(() => { 29 | // todo = new Todo(); 30 | // todo.name = 'Feed the cats'; 31 | // todo.uid = '123'; 32 | // fixture = TestBed.createComponent(DisplayTodoComponent); 33 | // component = fixture.componentInstance; 34 | // component.todo = todo; 35 | // fixture.detectChanges(); 36 | // }); 37 | 38 | // it('emits the edit action with it\'s todo uid WHEN the todo is clicked', (done) => { 39 | // spyOn(MockStateService.prototype, 'dispatch'); 40 | // (fixture.nativeElement as HTMLElement) 41 | // .querySelector('[data-e2e="todo-name-123"]') 42 | // .dispatchEvent(new MouseEvent('click')); 43 | // expect(MockStateService.prototype.dispatch) 44 | // .toHaveBeenCalledWith(new TodosActions.Edit('123')); 45 | // done(); 46 | // }); 47 | 48 | // it('emits the delte action with it\'s todo uid WHEN delete button is clicked', (done) => { 49 | // spyOn(MockStateService.prototype, 'dispatch'); 50 | // (fixture.nativeElement as HTMLElement) 51 | // .querySelector('[data-e2e="delete-todo-123"]') 52 | // .dispatchEvent(new MouseEvent('click')); 53 | // expect(MockStateService.prototype.dispatch) 54 | // .toHaveBeenCalledWith(new TodosActions.Delete('123')); 55 | // done(); 56 | // }); 57 | // }); 58 | -------------------------------------------------------------------------------- /src/app/account/todos/todo-display/todo-display.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | 3 | import { Todo } from '../todo'; 4 | import { Store } from '@ngrx/store'; 5 | import { AccountAppState } from 'app/account/state/store.config'; 6 | import { TodosActions } from 'app/account/todos/state/todos.actions'; 7 | 8 | @Component({ 9 | selector: 'app-todo-display', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | templateUrl: './todo-display.component.html' 12 | }) 13 | 14 | export class DisplayTodoComponent { 15 | 16 | @Input() todo = new Todo(); 17 | 18 | constructor(private state: Store) {} 19 | 20 | edit() { 21 | this.state.dispatch(new TodosActions.Edit(this.todo.uid)); 22 | } 23 | 24 | delete() { 25 | if (this.todo.uid) { 26 | this.state.dispatch(new TodosActions.Delete(this.todo.uid)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/account/todos/todo-edit/todo-edit.component.html: -------------------------------------------------------------------------------- 1 |
5 |
6 | 7 | 13 |
14 | 19 | 24 | 25 |
-------------------------------------------------------------------------------- /src/app/account/todos/todo-edit/todo-edit.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { NO_ERRORS_SCHEMA } from '@angular/core'; 2 | // import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | // import { Todo } from '../todo'; 4 | // import { Store } from '@ngrx/store'; 5 | // import { AppState, DefaultAppState, TodosActions } from '../../store'; 6 | // import { EditTodoComponent } from './todo-edit.component'; 7 | // import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 8 | // import { Observable } from 'rxjs/Observable'; 9 | // import { assignDeep } from '../../helpers'; 10 | 11 | // describe('Todo Edit Component', () => { 12 | // let component: EditTodoComponent; 13 | // let fixture: ComponentFixture; 14 | // let todo: Todo; 15 | 16 | // class MockStateService { 17 | // dispatch() {} 18 | // } 19 | 20 | // beforeEach(async(() => { 21 | // TestBed.configureTestingModule({ 22 | // imports: [ReactiveFormsModule], 23 | // declarations: [EditTodoComponent], 24 | // providers: [ 25 | // FormBuilder, 26 | // { provide: StateService, useClass: MockStateService } 27 | // ], 28 | // schemas: [NO_ERRORS_SCHEMA] 29 | // }) 30 | // .compileComponents(); 31 | // })); 32 | 33 | // beforeEach(() => { 34 | // todo = new Todo(); 35 | // todo.name = 'Feed the cats'; 36 | // todo.uid = '123'; 37 | // fixture = TestBed.createComponent(EditTodoComponent); 38 | // component = fixture.componentInstance; 39 | // component.todo = todo; 40 | // fixture.detectChanges(); 41 | // }); 42 | 43 | // it('emits the cancel action WHEN cancel button is clicked', () => { 44 | // spyOn(MockStateService.prototype, 'dispatch'); 45 | // (fixture.nativeElement as HTMLElement) 46 | // .querySelector('[data-e2e="todo-edit-cancel"]') 47 | // .dispatchEvent(new Event('click')); 48 | // expect(MockStateService.prototype.dispatch) 49 | // .toHaveBeenCalledWith(new TodosActions.CloseEdit()); 50 | // }); 51 | // }); 52 | -------------------------------------------------------------------------------- /src/app/account/todos/todo-edit/todo-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from '../todo'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | Input, 6 | OnInit, 7 | } from '@angular/core'; 8 | import { FormBuilder } from '@angular/forms'; 9 | import { Store } from '@ngrx/store'; 10 | import { minLength } from 'app/validators'; 11 | import { Messages } from 'app/resources/messages'; 12 | import { AccountAppState } from 'app/account/state/store.config'; 13 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 14 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 15 | import { TodosActions } from 'app/account/todos/state/todos.actions'; 16 | 17 | @Component({ 18 | selector: 'app-todo-edit', 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | templateUrl: './todo-edit.component.html', 21 | }) 22 | export class EditTodoComponent { 23 | @Input() todo: Todo = new Todo(); 24 | @Input() cancelable = true; 25 | 26 | formGroup = new TypedFormGroup({ 27 | name: new TypedFormControl( 28 | '', 29 | minLength(Messages.Validation.TodoNameMinLength)(1) 30 | ), 31 | }); 32 | 33 | constructor(private state: Store) {} 34 | 35 | save() { 36 | if (this.formGroup.valid) { 37 | this.state.dispatch( 38 | new TodosActions.Update({ 39 | ...this.todo, 40 | name: this.formGroup.value.name, 41 | }) 42 | ); 43 | this.formGroup.controls.name.reset(''); 44 | } 45 | } 46 | 47 | cancel() { 48 | this.state.dispatch(new TodosActions.CloseEdit()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/account/todos/todo.ts: -------------------------------------------------------------------------------- 1 | export class Todo { 2 | name = ''; 3 | uid: string | null = null; 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/account/todos/todo/todo.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/account/todos/todo/todo.component.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from '../todo'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | Input, 6 | OnInit, 7 | } from '@angular/core'; 8 | import { Observable } from 'rxjs/Observable'; 9 | import { Store } from '@ngrx/store'; 10 | import { AccountAppState } from 'app/account/state/store.config'; 11 | 12 | @Component({ 13 | selector: 'app-todo', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | templateUrl: './todo.component.html', 16 | }) 17 | export class TodoComponent implements OnInit { 18 | @Input() todo = new Todo(); 19 | isEditing$: Observable; 20 | 21 | constructor(private state: Store) {} 22 | 23 | ngOnInit() { 24 | this.isEditing$ = this.state.select( 25 | s => 26 | !this.todo.uid || 27 | !this.todo.name || 28 | s.account.todos.editing === this.todo.uid 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/account/todos/todos.module.ts: -------------------------------------------------------------------------------- 1 | import { DisplayTodoComponent } from './todo-display/todo-display.component'; 2 | import { TodosService } from './todos.service'; 3 | import { EditTodoComponent } from './todo-edit/todo-edit.component'; 4 | import { TodoComponent } from './todo/todo.component'; 5 | import { NgModule } from '@angular/core'; 6 | import { SharedModule } from 'app/shared/shared.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | SharedModule 11 | ], 12 | declarations: [ 13 | TodoComponent, 14 | EditTodoComponent, 15 | DisplayTodoComponent 16 | ], 17 | providers : [ 18 | TodosService 19 | ], 20 | exports: [ 21 | TodoComponent, 22 | EditTodoComponent, 23 | DisplayTodoComponent 24 | ] 25 | }) 26 | 27 | export class TodosModule {} 28 | -------------------------------------------------------------------------------- /src/app/account/todos/todos.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFireAuth } from 'angularfire2/auth'; 3 | import { 4 | AngularFireDatabase, 5 | AngularFireList, 6 | AngularFireObject, 7 | } from 'angularfire2/database'; 8 | 9 | import { Todo } from './todo'; 10 | 11 | /** 12 | * Standardises how todos are accessed 13 | */ 14 | 15 | @Injectable() 16 | export class TodosService { 17 | constructor( 18 | private db: AngularFireDatabase, 19 | private auth: AngularFireAuth 20 | ) {} 21 | 22 | todos(): AngularFireList { 23 | return this.db.list(`/todos/${this.auth.auth.currentUser.uid}`); 24 | } 25 | 26 | todo(uid: string): AngularFireObject { 27 | return this.db.object( 28 | `/todos/${this.auth.auth.currentUser.uid}/${uid}` 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/account/user/infoPage/infoPage.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Your Account

3 | 6 | 7 |

8 | No profile picture 9 | 12 |

13 | 14 | 15 | 16 | 19 |

20 | {{ (user$ | async)?.email }} 21 | 24 |

25 |
26 | 27 | 28 | 29 | 32 |

33 | 36 |

37 | 38 | 39 | 40 | 41 |

{{ (user$ | async)?.emailVerified ? 'Yes' : 'No' }}

42 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/app/account/user/infoPage/infoPage.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { AngularFireAuth } from 'angularfire2/auth'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { Store } from '@ngrx/store'; 6 | import { AccountAppState } from 'app/account/state/store.config'; 7 | import { UserState } from 'app/store/user/user.state'; 8 | import { UpdatePhotoUrlActions } from 'app/account/user/update-photo-url/state/updatePhotoUrl.actions'; 9 | import { UpdatePasswordActions } from 'app/account/user/update-password/state/updatePassword.actions'; 10 | import { UpdateEmailActions } from 'app/account/user/update-email/state/updateEmail.actions'; 11 | import { ResendEmailVerificationActions } from 'app/account/user/send-email-verification/state/resendEmailVerification.actions'; 12 | 13 | @Component({ 14 | selector: 'app-account-info-page', 15 | templateUrl: './infoPage.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class InfoPageComponent { 19 | user$ = this.auth.authState; 20 | userState$: Observable = this.state.select(s => s.user); 21 | 22 | constructor( 23 | protected state: Store, 24 | protected auth: AngularFireAuth 25 | ) {} 26 | 27 | toggleUpdatePhotoUrl() { 28 | this.state.dispatch(new UpdatePhotoUrlActions.ToggleForm()); 29 | } 30 | 31 | toggleUpdatePasswordForm() { 32 | this.state.dispatch(new UpdatePasswordActions.ToggleForm()); 33 | } 34 | 35 | toggleUpdateEmailForm() { 36 | this.state.dispatch(new UpdateEmailActions.ToggleForm()); 37 | } 38 | 39 | resendEmailVerification() { 40 | this.state.dispatch(new ResendEmailVerificationActions.Resend()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/form/sendEmailVerification.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Send verification email 4 | 5 | {{(formState$ | async).successMessage}} 6 | {{(formState$ | async).failureMessage}} 7 |
8 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/form/sendEmailVerification.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { AppState } from 'app/store/app.state'; 4 | import { AccountAppState } from 'app/account/state/store.config'; 5 | import { ResendEmailVerificationActions } from 'app/account/user/send-email-verification/state/resendEmailVerification.actions'; 6 | import { UserAppState } from 'app/account/user/state/store.config'; 7 | 8 | @Component({ 9 | selector: 'app-account-send-email-verification', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | templateUrl: './sendEmailVerification.component.html', 12 | }) 13 | export class SendEmailVerificationComponent { 14 | formState$ = this.state.select(s => s.accountUser.sendEmailVerification); 15 | 16 | constructor(private state: Store) {} 17 | 18 | send() { 19 | this.state.dispatch(new ResendEmailVerificationActions.Resend()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/sendEmailVerification.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SendEmailVerificationComponent } from 'app/account/user/send-email-verification/form/sendEmailVerification.component'; 3 | import { AccountSharedModule } from 'app/account/shared/accountShared.module'; 4 | 5 | @NgModule({ 6 | imports: [AccountSharedModule], 7 | declarations: [SendEmailVerificationComponent], 8 | exports: [SendEmailVerificationComponent], 9 | }) 10 | export class SendEmailVerificationModule {} 11 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/state/initialState.ts: -------------------------------------------------------------------------------- 1 | import { FormStates } from 'app/store/forms/formState'; 2 | 3 | export const initialResendVerificationEmailState = FormStates.Default; 4 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/state/resendEmailVerification.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export enum ResendEmailVerificationActionTypes { 2 | Resend = '[SendEmailVerification] Send', 3 | Success = '[SendEmailVerification] Success', 4 | Failure = '[SendEmailVerification] Failure', 5 | } 6 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/state/resendEmailVerification.actions.ts: -------------------------------------------------------------------------------- 1 | import { ResendEmailVerificationActionTypes } from './resendEmailVerification.actionTypes'; 2 | import { Action } from '@ngrx/store'; 3 | 4 | export namespace ResendEmailVerificationActions { 5 | export class Resend implements Action { 6 | readonly type = ResendEmailVerificationActionTypes.Resend; 7 | } 8 | 9 | export class Failure implements Action { 10 | readonly type = ResendEmailVerificationActionTypes.Failure; 11 | constructor(public readonly payload: any) {} 12 | } 13 | 14 | export class Success implements Action { 15 | readonly type = ResendEmailVerificationActionTypes.Success; 16 | constructor(public readonly payload: any) {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/state/resendEmailVerification.effects.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/operator/filter'; 2 | 3 | import { Injectable } from '@angular/core'; 4 | import { Effect, Actions } from '@ngrx/effects'; 5 | import { AngularFireAuth } from 'angularfire2/auth'; 6 | import { Observable } from 'rxjs/Observable'; 7 | 8 | import { Store } from '@ngrx/store'; 9 | import { AccountAppState } from 'app/account/state/store.config'; 10 | import { ResendEmailVerificationActionTypes } from 'app/account/user/send-email-verification/state/resendEmailVerification.actionTypes'; 11 | import { ResendEmailVerificationActions } from 'app/account/user/send-email-verification/state/resendEmailVerification.actions'; 12 | @Injectable() 13 | export class ResendEmailVerificationEffects { 14 | @Effect() 15 | sendEmailVerification$ = this.actions$ 16 | .ofType(ResendEmailVerificationActionTypes.Resend) 17 | .switchMap(() => 18 | this.auth.authState 19 | .first() 20 | .filter(a => !!a && !a.isAnonymous) 21 | .switchMap(a => 22 | Observable.from(a.sendEmailVerification()) 23 | .map( 24 | res => 25 | new ResendEmailVerificationActions.Success(res) 26 | ) 27 | .catch(error => 28 | Observable.of( 29 | new ResendEmailVerificationActions.Failure( 30 | error 31 | ) 32 | ) 33 | ) 34 | ) 35 | ); 36 | 37 | constructor( 38 | private actions$: Actions, 39 | private state: Store, 40 | private auth: AngularFireAuth 41 | ) {} 42 | } 43 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/state/resendEmailVerification.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ResendEmailVerificationActionTypes } from './resendEmailVerification.actionTypes'; 2 | import { formReducer } from 'app/store/forms/form.reducer.factory'; 3 | import { Messages } from 'app/resources/messages'; 4 | import { FormState } from 'app/store/forms/formState'; 5 | import { ResendEmailVerificationActions } from 'app/account/user/send-email-verification/state/resendEmailVerification.actions'; 6 | import { Action } from '@ngrx/store'; 7 | 8 | export function resendEmailVerificationReducer(state: FormState, action: Action) { 9 | return formReducer({ 10 | request: ResendEmailVerificationActionTypes.Resend, 11 | failure: ResendEmailVerificationActionTypes.Failure, 12 | success: ResendEmailVerificationActionTypes.Success, 13 | successMessage: Messages.ApiResponse.SendVerificationEmailSuccess, 14 | })(state, action); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/account/user/send-email-verification/state/resendEmailVerificationState.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from 'app/store/forms/formState'; 2 | 3 | export type ResendEmailVerificationState = FormState; 4 | -------------------------------------------------------------------------------- /src/app/account/user/state/store.config.ts: -------------------------------------------------------------------------------- 1 | import { ResendEmailVerificationState } from 'app/account/user/send-email-verification/state/resendEmailVerificationState'; 2 | import { UpdateEmailState } from 'app/account/user/update-email/state/updateEmailState'; 3 | import { AppFeatureState } from 'app/store/utils/featureState'; 4 | import { AppState } from 'app/store/app.state'; 5 | import { ActionReducerMap } from '@ngrx/store'; 6 | import { resendEmailVerificationReducer } from 'app/account/user/send-email-verification/state/resendEmailVerification.reducer'; 7 | import { updateEmailReducer } from 'app/account/user/update-email/state/updateEmail.reducer'; 8 | import { UpdatePasswordState } from 'app/account/user/update-password/state/updatePasswordState'; 9 | import { updatePasswordReducer } from 'app/account/user/update-password/state/updatePassword.reducer'; 10 | import { UpdatePhotoUrlState } from 'app/account/user/update-photo-url/state/updatePhotoUrlState'; 11 | import { initialResendVerificationEmailState } from 'app/account/user/send-email-verification/state/initialState'; 12 | import { initialUpdateEmailState } from 'app/account/user/update-email/state/initialState'; 13 | import { initialUpdatePasswordState } from 'app/account/user/update-password/state/initialState'; 14 | import { initialUpdatePhotoUrlState } from 'app/account/user/update-photo-url/state/initialState'; 15 | import { updatePhotoUrlReducer } from 'app/account/user/update-photo-url/state/updatePhotoUrl.reducer'; 16 | 17 | export const USER_STORE_KEY = 'accountUser'; 18 | 19 | export interface UserFeatureState { 20 | sendEmailVerification: ResendEmailVerificationState; 21 | updateEmail: UpdateEmailState; 22 | updatePassword: UpdatePasswordState; 23 | updatePhotoUrl: UpdatePhotoUrlState; 24 | } 25 | 26 | export type UserAppState = AppFeatureState< 27 | AppState, 28 | typeof USER_STORE_KEY, 29 | UserFeatureState 30 | >; 31 | 32 | export const userReducers: ActionReducerMap = { 33 | sendEmailVerification: resendEmailVerificationReducer, 34 | updateEmail: updateEmailReducer, 35 | updatePassword: updatePasswordReducer, 36 | updatePhotoUrl: updatePhotoUrlReducer, 37 | }; 38 | 39 | export const initialUserState: UserFeatureState = { 40 | sendEmailVerification: initialResendVerificationEmailState, 41 | updateEmail: initialUpdateEmailState, 42 | updatePassword: initialUpdatePasswordState, 43 | updatePhotoUrl: initialUpdatePhotoUrlState, 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/account/user/state/userState.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { StoreModule } from '@ngrx/store'; 3 | import { 4 | USER_STORE_KEY, 5 | userReducers, 6 | initialUserState, 7 | } from 'app/account/user/state/store.config'; 8 | import { initialAccountState } from 'app/account/state/store.config'; 9 | import { EffectsModule } from '@ngrx/effects'; 10 | import { ResendEmailVerificationEffects } from 'app/account/user/send-email-verification/state/resendEmailVerification.effects'; 11 | import { UpdatePhotoUrlEffects } from 'app/account/user/update-photo-url/state/updatePhotoUrl.effects'; 12 | import { UpdateEmailEffects } from 'app/account/user/update-email/state/updateEmail.effects'; 13 | import { UpdatePasswordEffects } from 'app/account/user/update-password/state/updatePassword.effects'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | StoreModule.forFeature(USER_STORE_KEY, userReducers, { 18 | initialState: initialUserState, 19 | }), 20 | EffectsModule.forFeature([ 21 | ResendEmailVerificationEffects, 22 | UpdatePhotoUrlEffects, 23 | UpdateEmailEffects, 24 | UpdatePasswordEffects, 25 | ]), 26 | ], 27 | }) 28 | export class UserStateModule {} 29 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/form/updateEmail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 | 8 | 9 | Update 10 | 11 |

12 | {{(formState$ | async).successMessage}} 13 |

14 |

15 | {{(formState$ | async).failureMessage}} 16 |

17 |
-------------------------------------------------------------------------------- /src/app/account/user/update-email/form/updateEmail.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { AccountAppState } from 'app/account/state/store.config'; 4 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 5 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 6 | import { emailValid } from 'app/validators'; 7 | import { UserAppState } from 'app/account/user/state/store.config'; 8 | import { UpdateEmailActions } from 'app/account/user/update-email/state/updateEmail.actions'; 9 | 10 | @Component({ 11 | selector: 'app-account-update-email', 12 | templateUrl: './updateEmail.component.html', 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class UpdateEmailComponent { 16 | formGroup = new TypedFormGroup({ 17 | newEmail: new TypedFormControl('', emailValid), 18 | }); 19 | 20 | formState$ = this.state.select(s => s.accountUser.updateEmail); 21 | 22 | constructor(private state: Store) {} 23 | 24 | updateEmail() { 25 | this.state.dispatch( 26 | new UpdateEmailActions.Update(this.formGroup.value.newEmail) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/initialState.ts: -------------------------------------------------------------------------------- 1 | import { FormStates } from 'app/store/forms/formState'; 2 | import { UpdateEmailState } from 'app/account/user/update-email/state/updateEmailState'; 3 | 4 | export const initialUpdateEmailState: UpdateEmailState = FormStates.Default; 5 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/updateEmail.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace UpdateEmailActionTypes { 2 | export const ToggleForm = '[UpdateEmail] Toggle Form'; 3 | export const Update = '[UpdateEmail] Update Email'; 4 | export const Success = '[UpdateEmail] Succsess'; 5 | export const Failure = '[UpdateEmail] Failure'; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/updateEmail.actions.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmailActionTypes } from './updateEmail.actionTypes'; 2 | import { Action } from '@ngrx/store'; 3 | 4 | export namespace UpdateEmailActions { 5 | export class ToggleForm implements Action { 6 | readonly type = UpdateEmailActionTypes.ToggleForm; 7 | payload: void; 8 | } 9 | 10 | export class Update implements Action { 11 | readonly type = UpdateEmailActionTypes.Update; 12 | // payload: new Email 13 | constructor(public readonly payload: string) {} 14 | } 15 | 16 | export class Failure implements Action { 17 | readonly type = UpdateEmailActionTypes.Failure; 18 | constructor(public readonly payload: any) {} 19 | } 20 | 21 | export class Success implements Action { 22 | readonly type = UpdateEmailActionTypes.Success; 23 | constructor(public readonly payload: any) {} // TODO: type? 24 | } 25 | 26 | export type UpdateEmailAction = ToggleForm | Update | Failure | Success; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/updateEmail.effects.spec.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bave8672/angular-firebase-starter/3cb4873c40d58964ce79d1d9bf400659f1154207/src/app/account/user/update-email/state/updateEmail.effects.spec.ts -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/updateEmail.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Effect, Actions } from '@ngrx/effects'; 3 | import { AngularFireAuth } from 'angularfire2/auth'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { Store } from '@ngrx/store'; 7 | import { AccountAppState } from 'app/account/state/store.config'; 8 | import { UpdateEmailActionTypes } from 'app/account/user/update-email/state/updateEmail.actionTypes'; 9 | import { UpdateEmailActions } from 'app/account/user/update-email/state/updateEmail.actions'; 10 | 11 | @Injectable() 12 | export class UpdateEmailEffects { 13 | @Effect() 14 | updateEmail$ = this.actions$ 15 | .ofType(UpdateEmailActionTypes.Update) 16 | .map((action: UpdateEmailActions.Update) => action.payload) 17 | .switchMap(newEmail => 18 | this.auth.authState 19 | .first() 20 | .filter(a => !a.isAnonymous) 21 | .switchMap(a => a.updateEmail(newEmail)) 22 | .map(res => new UpdateEmailActions.Success(res)) 23 | .catch(error => 24 | Observable.of(new UpdateEmailActions.Failure(error)) 25 | ) 26 | ); 27 | 28 | constructor(private actions$: Actions, private auth: AngularFireAuth) {} 29 | } 30 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/updateEmail.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormState, FormStates } from 'app/store/forms/formState'; 2 | 3 | import { updateEmailReducer } from './updateEmail.reducer'; 4 | import { shouldNotAlterStateOnUnknownAction } from 'app/store/testing'; 5 | import { assignDeep } from 'app/helpers'; 6 | import { Messages } from 'app/resources/messages'; 7 | import { UpdateEmailActions } from 'app/account/user/update-email/state/updateEmail.actions'; 8 | 9 | describe('Update Email Reducer', () => { 10 | shouldNotAlterStateOnUnknownAction(updateEmailReducer); 11 | 12 | let oldState: FormState; 13 | 14 | beforeEach(() => { 15 | oldState = assignDeep(FormStates.Default); 16 | }); 17 | 18 | it('Should toggle the form visibility when toggle is called', () => { 19 | oldState.showForm = false; 20 | let newState = updateEmailReducer( 21 | oldState, 22 | new UpdateEmailActions.ToggleForm() 23 | ); 24 | expect(newState.showForm).toBe(true); 25 | newState = updateEmailReducer( 26 | newState, 27 | new UpdateEmailActions.ToggleForm() 28 | ); 29 | expect(newState.showForm).toBe(false); 30 | }); 31 | 32 | it('Should show the requesting status WHEN update is called', () => { 33 | const newState = updateEmailReducer( 34 | oldState, 35 | new UpdateEmailActions.Update('example@gmail.com') 36 | ); 37 | expect(newState).toEqual(FormStates.Requesting); 38 | }); 39 | 40 | it('Should show the failure status WHEN failure is called', () => { 41 | const newState = updateEmailReducer( 42 | oldState, 43 | new UpdateEmailActions.Failure({}) 44 | ); 45 | expect(newState).toEqual( 46 | FormStates.Failure(Messages.ApiResponse.ServerError) 47 | ); 48 | }); 49 | 50 | it('Should show the success status WHEN failure is called', () => { 51 | const newState = updateEmailReducer( 52 | oldState, 53 | new UpdateEmailActions.Success({}) 54 | ); 55 | expect(newState).toEqual( 56 | FormStates.Success(Messages.ApiResponse.UpdateEmailSuccess) 57 | ); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/updateEmail.reducer.ts: -------------------------------------------------------------------------------- 1 | import { formReducer } from 'app/store/forms/form.reducer.factory'; 2 | import { UpdateEmailActionTypes } from 'app/account/user/update-email/state/updateEmail.actionTypes'; 3 | import { Messages } from 'app/resources/messages'; 4 | import { FormState } from 'app/store/forms/formState'; 5 | import { Action } from '@ngrx/store'; 6 | 7 | export function updateEmailReducer(state: FormState, action: Action) { 8 | return formReducer({ 9 | toggle: UpdateEmailActionTypes.ToggleForm, 10 | request: UpdateEmailActionTypes.Update, 11 | success: UpdateEmailActionTypes.Success, 12 | failure: UpdateEmailActionTypes.Failure, 13 | successMessage: Messages.ApiResponse.UpdateEmailSuccess, 14 | })(state, action); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/state/updateEmailState.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from 'app/store/forms/formState'; 2 | 3 | export type UpdateEmailState = FormState; 4 | -------------------------------------------------------------------------------- /src/app/account/user/update-email/updateEmail.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { UpdateEmailComponent } from 'app/account/user/update-email/form/updateEmail.component'; 3 | import { SharedModule } from 'app/shared/shared.module'; 4 | import { AccountSharedModule } from 'app/account/shared/accountShared.module'; 5 | 6 | @NgModule({ 7 | imports: [AccountSharedModule], 8 | declarations: [UpdateEmailComponent], 9 | exports: [UpdateEmailComponent], 10 | }) 11 | export class UpdateEmailModule {} 12 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/form/updatePassword.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | 6 | 10 | 11 |
12 |
13 | 14 | 18 | 19 |
20 |
21 | 22 | 26 | 27 |
28 | 29 | 32 | Update Password 33 | 34 |

36 | {{(formState$ | async).passwordUpdateSuccessMessage}} 37 |

38 |

40 | {{(formState$ | async).passwordUpdateFailureMessage}} 41 |

42 |
-------------------------------------------------------------------------------- /src/app/account/user/update-password/form/updatePassword.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FormBuilder } from '@angular/forms'; 3 | 4 | import { Store } from '@ngrx/store'; 5 | import { AccountAppState } from 'app/account/state/store.config'; 6 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 7 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 8 | import { UnequalValidationError } from 'app/validators/valuesEqual'; 9 | import { passwordValid, valuesEqual } from 'app/validators'; 10 | import { UpdatePasswordActions } from 'app/account/user/update-password/state/updatePassword.actions'; 11 | import { UserAppState } from 'app/account/user/state/store.config'; 12 | 13 | interface UpdatePasswordForm { 14 | password: string; 15 | newPassword: string; 16 | confirmNewPassword: string; 17 | } 18 | 19 | @Component({ 20 | selector: 'app-account-update-password', 21 | templateUrl: './updatePassword.component.html', 22 | changeDetection: ChangeDetectionStrategy.OnPush, 23 | }) 24 | export class UpdatePasswordComponent { 25 | formGroup: TypedFormGroup = new TypedFormGroup< 26 | UpdatePasswordForm 27 | >( 28 | { 29 | password: new TypedFormControl(''), 30 | newPassword: new TypedFormControl('', passwordValid), 31 | confirmNewPassword: new TypedFormControl(''), 32 | }, 33 | valuesEqual( 34 | () => this.formGroup.controls.newPassword, 35 | () => this.formGroup.controls.confirmNewPassword 36 | )() 37 | ); 38 | 39 | formState$ = this.state.select(s => s.accountUser.updatePassword); 40 | 41 | constructor(private state: Store) {} 42 | 43 | updatePassword() { 44 | this.state.dispatch( 45 | new UpdatePasswordActions.Update({ 46 | old: this.formGroup.value.password, 47 | new: this.formGroup.value.newPassword, 48 | }) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/state/initialState.ts: -------------------------------------------------------------------------------- 1 | import { UpdatePasswordState } from 'app/account/user/update-password/state/updatePasswordState'; 2 | import { FormStates } from 'app/store/forms/formState'; 3 | 4 | export const initialUpdatePasswordState: UpdatePasswordState = 5 | FormStates.Default; 6 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/state/updatePassword.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace UpdatePasswordActionTypes { 2 | export const ToggleForm = '[UpdatePassword] Toggle Form'; 3 | export const Update = '[UpdatePassword] Send Request'; 4 | export const Failure = '[UpdatePassword] Failure'; 5 | export const Success = '[UpdatePassword] Success'; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/state/updatePassword.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { UpdatePasswordActionTypes } from 'app/account/user/update-password/state/updatePassword.actionTypes'; 3 | 4 | export namespace UpdatePasswordActions { 5 | 6 | export class ToggleForm implements Action { 7 | readonly type = UpdatePasswordActionTypes.ToggleForm; 8 | payload: void; 9 | } 10 | 11 | export class Update implements Action { 12 | readonly type = UpdatePasswordActionTypes.Update; 13 | constructor(public readonly payload: { 14 | old: string, 15 | new: string 16 | }) {} 17 | } 18 | 19 | export class Failure implements Action { 20 | readonly type = UpdatePasswordActionTypes.Failure; 21 | constructor(public readonly payload: any) {} 22 | } 23 | 24 | export class Success implements Action { 25 | readonly type = UpdatePasswordActionTypes.Success; 26 | constructor(public readonly payload: any) {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/state/updatePassword.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Effect, Actions } from '@ngrx/effects'; 3 | import { AngularFireAuth } from 'angularfire2/auth'; 4 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 5 | import { LogInActionTypes } from 'app/store/user/logIn/logIn.actionTypes'; 6 | import { Observable } from 'rxjs/Observable'; 7 | 8 | import { Store } from '@ngrx/store'; 9 | import { AccountAppState } from 'app/account/state/store.config'; 10 | import { UpdatePasswordActionTypes } from 'app/account/user/update-password/state/updatePassword.actionTypes'; 11 | import { UpdatePasswordActions } from 'app/account/user/update-password/state/updatePassword.actions'; 12 | 13 | @Injectable() 14 | export class UpdatePasswordEffects { 15 | @Effect() 16 | updatePassword$ = this.actions$ 17 | .ofType(UpdatePasswordActionTypes.Update) 18 | .map((action: UpdatePasswordActions.Update) => action.payload) 19 | .switchMap(passwords => { 20 | this.state.dispatch( 21 | new LogInActions.LogIn({ 22 | email: this.auth.auth.currentUser.email, 23 | password: passwords.old, 24 | }) 25 | ); 26 | 27 | return Observable.race( 28 | this.actions$ 29 | .ofType(LogInActionTypes.Success) 30 | .switchMap(() => 31 | Observable.from( 32 | this.auth.auth.currentUser.updatePassword( 33 | passwords.new 34 | ) 35 | ) 36 | .map(res => new UpdatePasswordActions.Success(res)) 37 | .catch(err => 38 | Observable.of( 39 | new UpdatePasswordActions.Failure(err) 40 | ) 41 | ) 42 | ), 43 | this.actions$ 44 | .ofType(LogInActionTypes.Failure) 45 | .map( 46 | action => 47 | new UpdatePasswordActions.Failure(action.payload) 48 | ) 49 | ); 50 | }); 51 | 52 | constructor( 53 | private actions$: Actions, 54 | private state: Store, 55 | private auth: AngularFireAuth 56 | ) {} 57 | } 58 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/state/updatePassword.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormState, FormStates } from 'app/store/forms/formState'; 2 | import { updatePasswordReducer } from './updatePassword.reducer'; 3 | import { shouldNotAlterStateOnUnknownAction } from 'app/store/testing'; 4 | import { assignDeep } from 'app/helpers'; 5 | import { UpdatePasswordActions } from 'app/account/user/update-password/state/updatePassword.actions'; 6 | import { Messages } from 'app/resources/messages'; 7 | 8 | describe('Update Password Reducer', () => { 9 | shouldNotAlterStateOnUnknownAction(updatePasswordReducer); 10 | 11 | let oldState: FormState; 12 | 13 | beforeEach(() => { 14 | oldState = assignDeep(FormStates.Default); 15 | }); 16 | 17 | it('Toggles the form visibility WHEN Toggle is called', () => { 18 | oldState.showForm = false; 19 | 20 | let newState = updatePasswordReducer( 21 | oldState, 22 | new UpdatePasswordActions.ToggleForm() 23 | ); 24 | expect(newState.showForm).toBe(true); 25 | 26 | newState = updatePasswordReducer( 27 | newState, 28 | new UpdatePasswordActions.ToggleForm() 29 | ); 30 | expect(newState.showForm).toBe(false); 31 | }); 32 | 33 | it('Assigns the requesting state WHEN UpdatePassword is called', () => { 34 | const newState = updatePasswordReducer( 35 | oldState, 36 | new UpdatePasswordActions.Update({} as any) 37 | ); 38 | expect(newState).toEqual(FormStates.Requesting); 39 | }); 40 | 41 | it('Displays the correct error message WHEN failure is called', () => { 42 | const newState = updatePasswordReducer( 43 | oldState, 44 | new UpdatePasswordActions.Failure({}) 45 | ); 46 | expect(newState).toEqual( 47 | FormStates.Failure(Messages.ApiResponse.ServerError) 48 | ); 49 | }); 50 | 51 | it('Displays the correct message WHEN success is called', () => { 52 | const newState = updatePasswordReducer( 53 | oldState, 54 | new UpdatePasswordActions.Success({}) 55 | ); 56 | expect(newState).toEqual( 57 | FormStates.Success(Messages.ApiResponse.UpdatePasswordSuccess) 58 | ); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/state/updatePassword.reducer.ts: -------------------------------------------------------------------------------- 1 | import { formReducer } from 'app/store/forms/form.reducer.factory'; 2 | 3 | import { UpdatePasswordActionTypes } from './updatePassword.actionTypes'; 4 | import { Messages } from 'app/resources/messages'; 5 | import { FormState } from 'app/store/forms/formState'; 6 | import { Action } from '@ngrx/store'; 7 | 8 | export function updatePasswordReducer( 9 | state: FormState, 10 | action: Action 11 | ): FormState { 12 | return formReducer({ 13 | toggle: UpdatePasswordActionTypes.ToggleForm, 14 | request: UpdatePasswordActionTypes.Update, 15 | failure: UpdatePasswordActionTypes.Failure, 16 | success: UpdatePasswordActionTypes.Success, 17 | successMessage: Messages.ApiResponse.UpdatePasswordSuccess, 18 | failureMessage: Messages.ApiResponse.ServerError, 19 | })(state, action); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/state/updatePasswordState.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from 'app/store/forms/formState'; 2 | 3 | export type UpdatePasswordState = FormState; 4 | -------------------------------------------------------------------------------- /src/app/account/user/update-password/updatePassword.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { UpdatePasswordComponent } from 'app/account/user/update-password/form/updatePassword.component'; 3 | import { AccountSharedModule } from 'app/account/shared/accountShared.module'; 4 | 5 | @NgModule({ 6 | imports: [AccountSharedModule], 7 | declarations: [UpdatePasswordComponent], 8 | exports: [UpdatePasswordComponent], 9 | }) 10 | export class UpdatePasswordModule {} 11 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/form/updatePhoto-url.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | 6 |
7 |
8 | 9 | 13 |
14 | Save Profile Picture 17 |
-------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/form/updatePhoto-url.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FormBuilder } from '@angular/forms'; 3 | 4 | import { Store } from '@ngrx/store'; 5 | import { AccountAppState } from 'app/account/state/store.config'; 6 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 7 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 8 | import { validUrl } from 'app/validators/validUrl'; 9 | import { UpdatePhotoUrlActions } from 'app/account/user/update-photo-url/state/updatePhotoUrl.actions'; 10 | import { UserAppState } from 'app/account/user/state/store.config'; 11 | 12 | @Component({ 13 | selector: 'app-account-update-photo-url', 14 | templateUrl: './updatePhoto-url.component.html', 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | }) 17 | export class UpdatePhotoUrlComponent { 18 | formGroup = new TypedFormGroup({ 19 | newPhotoUrl: new TypedFormControl( 20 | '', 21 | validUrl('Please enter a vlaid URL') 22 | ), 23 | }); 24 | 25 | formState$ = this.state.select(s => s.accountUser.updatePhotoUrl); 26 | 27 | constructor(private state: Store) {} 28 | 29 | onSubmitPhoto() { 30 | this.state.dispatch(new UpdatePhotoUrlActions.Update(this.formGroup.value.newPhotoUrl)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/state/initialState.ts: -------------------------------------------------------------------------------- 1 | import { UpdatePhotoUrlState } from 'app/account/user/update-photo-url/state/updatePhotoUrlState'; 2 | import { FormStates } from 'app/store/forms/formState'; 3 | 4 | export const initialUpdatePhotoUrlState: UpdatePhotoUrlState = 5 | FormStates.Default; 6 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/state/updatePhotoUrl.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace UpdatePhotoUrlActionTypes { 2 | export const ToggleForm = '[UpdatePhotoUrl] Toggle Form'; 3 | export const Update = '[UpdatePhotoUrl] Update'; 4 | export const Failure = '[UpdatePhotoUrl] Failure'; 5 | export const Success = '[UpdatePhotoUrl] Success'; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/state/updatePhotoUrl.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { UpdatePhotoUrlActionTypes } from 'app/account/user/update-photo-url/state/updatePhotoUrl.actionTypes'; 3 | 4 | export namespace UpdatePhotoUrlActions { 5 | export class ToggleForm implements Action { 6 | readonly type = UpdatePhotoUrlActionTypes.ToggleForm; 7 | } 8 | 9 | export class Update implements Action { 10 | readonly type = UpdatePhotoUrlActionTypes.Update; 11 | constructor(public readonly payload: string) {} 12 | } 13 | 14 | export class Failure implements Action { 15 | readonly type = UpdatePhotoUrlActionTypes.Failure; 16 | constructor(public readonly payload: any) {} 17 | } 18 | 19 | export class Success implements Action { 20 | readonly type = UpdatePhotoUrlActionTypes.Success; 21 | constructor(public readonly payload: any) {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/state/updatePhotoUrl.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Effect } from '@ngrx/effects'; 3 | import { AngularFireAuth } from 'angularfire2/auth'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { Store } from '@ngrx/store'; 7 | import { AccountAppState } from 'app/account/state/store.config'; 8 | import { Actions } from '@ngrx/effects'; 9 | import { UpdatePhotoUrlActionTypes } from 'app/account/user/update-photo-url/state/updatePhotoUrl.actionTypes'; 10 | import { UpdatePhotoUrlActions } from 'app/account/user/update-photo-url/state/updatePhotoUrl.actions'; 11 | 12 | @Injectable() 13 | export class UpdatePhotoUrlEffects { 14 | @Effect() 15 | updatePhotoUrl$ = this.actions$ 16 | .ofType(UpdatePhotoUrlActionTypes.Update) 17 | .map((action: UpdatePhotoUrlActions.Update) => action.payload) 18 | .switchMap(url => 19 | this.auth.authState 20 | .switchMap(authState => { 21 | return Observable.from( 22 | authState.updateProfile({ 23 | displayName: authState.displayName, 24 | photoURL: url, 25 | }) 26 | ); 27 | }) 28 | .map(res => new UpdatePhotoUrlActions.Success(res)) 29 | .catch(error => 30 | Observable.of(new UpdatePhotoUrlActions.Failure(error)) 31 | ) 32 | ); 33 | 34 | constructor(private actions$: Actions, private auth: AngularFireAuth) {} 35 | } 36 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/state/updatePhotoUrl.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormState, FormStates } from 'app/store/forms/formState'; 2 | import { shouldNotAlterStateOnUnknownAction } from 'app/store/testing'; 3 | import { assignDeep } from 'app/helpers'; 4 | import { UpdatePhotoUrlActions } from 'app/account/user/update-photo-url/state/updatePhotoUrl.actions'; 5 | import { updatePhotoUrlReducer } from 'app/account/user/update-photo-url/state/updatePhotoUrl.reducer'; 6 | import { Messages } from 'app/resources/messages'; 7 | 8 | describe('Update Photo Url Reducer', () => { 9 | shouldNotAlterStateOnUnknownAction(updatePhotoUrlReducer); 10 | 11 | let oldState: FormState; 12 | 13 | beforeEach(() => { 14 | oldState = assignDeep(FormStates.Default); 15 | }); 16 | 17 | it('Toggles the form visibility WHEN Toggle is called', () => { 18 | oldState.showForm = false; 19 | 20 | let newState = updatePhotoUrlReducer( 21 | oldState, 22 | new UpdatePhotoUrlActions.ToggleForm() 23 | ); 24 | expect(newState.showForm).toBe(true); 25 | 26 | newState = updatePhotoUrlReducer( 27 | newState, 28 | new UpdatePhotoUrlActions.ToggleForm() 29 | ); 30 | expect(newState.showForm).toBe(false); 31 | }); 32 | 33 | it('Assigns the requesting state WHEN UpdatePhotoUrl is called', () => { 34 | const newState = updatePhotoUrlReducer( 35 | oldState, 36 | new UpdatePhotoUrlActions.Update('') 37 | ); 38 | expect(newState).toEqual(FormStates.Requesting); 39 | }); 40 | 41 | it('Displays the correct error message WHEN failure is called', () => { 42 | const newState = updatePhotoUrlReducer( 43 | oldState, 44 | new UpdatePhotoUrlActions.Failure({}) 45 | ); 46 | expect(newState).toEqual( 47 | FormStates.Failure(Messages.ApiResponse.ServerError) 48 | ); 49 | }); 50 | 51 | it('Displays the correct message WHEN success is called', () => { 52 | const newState = updatePhotoUrlReducer( 53 | oldState, 54 | new UpdatePhotoUrlActions.Success({}) 55 | ); 56 | expect(newState).toEqual( 57 | FormStates.Success(Messages.ApiResponse.UpdatePhotoUrlSucess) 58 | ); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/state/updatePhotoUrl.reducer.ts: -------------------------------------------------------------------------------- 1 | import { formReducer } from 'app/store/forms/form.reducer.factory'; 2 | import { UpdatePhotoUrlActionTypes } from './updatePhotoUrl.actionTypes'; 3 | import { Messages } from 'app/resources/messages'; 4 | import { FormState } from 'app/store/forms/formState'; 5 | import { Action } from '@ngrx/store'; 6 | 7 | export function updatePhotoUrlReducer( 8 | state: FormState, 9 | action: Action 10 | ): FormState { 11 | return formReducer({ 12 | toggle: UpdatePhotoUrlActionTypes.ToggleForm, 13 | request: UpdatePhotoUrlActionTypes.Update, 14 | failure: UpdatePhotoUrlActionTypes.Failure, 15 | success: UpdatePhotoUrlActionTypes.Success, 16 | successMessage: Messages.ApiResponse.UpdatePhotoUrlSucess, 17 | })(state, action); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/state/updatePhotoUrlState.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from 'app/store/forms/formState'; 2 | 3 | export type UpdatePhotoUrlState = FormState; 4 | -------------------------------------------------------------------------------- /src/app/account/user/update-photo-url/updatePhotoUrl.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { UpdatePhotoUrlComponent } from 'app/account/user/update-photo-url/form/updatePhoto-url.component'; 3 | import { AccountSharedModule } from 'app/account/shared/accountShared.module'; 4 | 5 | @NgModule({ 6 | imports: [AccountSharedModule], 7 | declarations: [UpdatePhotoUrlComponent], 8 | exports: [UpdatePhotoUrlComponent], 9 | }) 10 | export class UpdatePhotoUrlModule {} 11 | -------------------------------------------------------------------------------- /src/app/account/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { UpdateEmailModule } from 'app/account/user/update-email/updateEmail.module'; 4 | import { InfoPageComponent } from 'app/account/user/infoPage/infoPage.component'; 5 | import { UserStateModule } from 'app/account/user/state/userState.module'; 6 | import { AccountSharedModule } from 'app/account/shared/accountShared.module'; 7 | import { UpdatePhotoUrlModule } from 'app/account/user/update-photo-url/updatePhotoUrl.module'; 8 | import { UpdatePasswordModule } from 'app/account/user/update-password/updatePassword.module'; 9 | import { SendEmailVerificationModule } from 'app/account/user/send-email-verification/sendEmailVerification.module'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | AccountSharedModule, 14 | RouterModule.forChild([ 15 | { path: '', pathMatch: 'full', component: InfoPageComponent }, 16 | ]), 17 | UserStateModule, 18 | UpdateEmailModule, 19 | UpdatePhotoUrlModule, 20 | UpdatePasswordModule, 21 | SendEmailVerificationModule, 22 | ], 23 | declarations: [InfoPageComponent], 24 | }) 25 | export class UserModule {} 26 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { GlobalActions } from 'app/store/global/global.actions'; 3 | 4 | import { Store } from '@ngrx/store'; 5 | import { ChangeDetectionStrategy } from '@angular/core'; 6 | import { AppState } from 'app/store/app.state'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class AppComponent implements OnInit { 14 | constructor(private state: Store) {} 15 | 16 | ngOnInit() { 17 | this.state.dispatch(new GlobalActions.AppStart()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { AngularFireModule } from 'angularfire2'; 4 | import { AngularFireAuthModule } from 'angularfire2/auth'; 5 | import { AngularFireDatabaseModule } from 'angularfire2/database'; 6 | 7 | import { environment } from '../environments/environment'; 8 | import { AccountModule } from './account/account.module'; 9 | import { AppComponent } from './app.component'; 10 | import { CustomErrorHandler } from './error-handler/custom-error-handler'; 11 | import { FooterComponent } from './footer/footer.component'; 12 | import { LandingPageComponent } from './landing-page/landing-page.component'; 13 | import { LogInComponent } from './log-in/log-in.component'; 14 | import { NavComponent } from './nav/nav.component'; 15 | import { SharedModule } from './shared/shared.module'; 16 | import { Store } from '@ngrx/store'; 17 | import { StateModule } from './store/state.module'; 18 | import { AppRoutingModule } from 'app/routing/appRouting.module'; 19 | import { BrowserModule } from '@angular/platform-browser'; 20 | 21 | @NgModule({ 22 | declarations: [ 23 | AppComponent, 24 | FooterComponent, 25 | LandingPageComponent, 26 | NavComponent, 27 | LogInComponent, 28 | ], 29 | imports: [ 30 | BrowserModule, 31 | SharedModule, 32 | AppRoutingModule, 33 | AngularFireModule.initializeApp(environment.firebaseConfig), 34 | AngularFireDatabaseModule, 35 | AngularFireAuthModule, 36 | StateModule, 37 | AccountModule, 38 | ], 39 | providers: [{ provide: ErrorHandler, useClass: CustomErrorHandler }], 40 | bootstrap: [AppComponent], 41 | }) 42 | export class AppModule {} 43 | -------------------------------------------------------------------------------- /src/app/error-handler/custom-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Injectable } from '@angular/core'; 2 | import { AngularFireDatabase } from 'angularfire2/database'; 3 | 4 | import { environment } from '../../environments/environment'; 5 | 6 | /** 7 | * Override for the globlal angular error handler. 8 | * 9 | * Calls through to the default handler in development, 10 | * posts the error to firebase in production. 11 | */ 12 | @Injectable() 13 | export class CustomErrorHandler extends ErrorHandler { 14 | constructor(private db: AngularFireDatabase) { 15 | super(); 16 | } 17 | 18 | handleError(error: Error) { 19 | if (!environment.production) { 20 | super.handleError(error); 21 | } else { 22 | const errorString = JSON.stringify( 23 | error, 24 | Object.getOwnPropertyNames(error) 25 | ); 26 | this.db.list('/errors').push(errorString); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/firebase/firebase.config.ts: -------------------------------------------------------------------------------- 1 | export const FirebaseConfig = { 2 | apiKey: 'AIzaSyC0byihBo8yqZJoqf1g6c2t8h7Pm7s92lg', 3 | authDomain: 'adviewer-73e3f.firebaseapp.com', 4 | databaseURL: 'https://adviewer-73e3f.firebaseio.com', 5 | storageBucket: 'adviewer-73e3f.appspot.com', 6 | messagingSenderId: '124306978102' 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/firebase/index.ts: -------------------------------------------------------------------------------- 1 | export * from './firebase.config'; 2 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

© 2017 Company, Inc.

5 |
6 |
-------------------------------------------------------------------------------- /src/app/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FooterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FooterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ChangeDetectionStrategy } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-footer', 6 | templateUrl: './footer.component.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | }) 9 | export class FooterComponent {} 10 | -------------------------------------------------------------------------------- /src/app/helpers/actionMap.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer } from '@ngrx/store'; 2 | 3 | export interface ActionMap { 4 | [type: string]: ActionReducer; 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/helpers/assign.spec.ts: -------------------------------------------------------------------------------- 1 | import { assign } from './'; 2 | describe('assign', () => { 3 | 4 | it(`Replaces target props with those from source 5 | AND does not modify the original objects`, () => { 6 | const targ = { 7 | x: 'x', 8 | y: 'y', 9 | }; 10 | const src = { 11 | x: 'X', 12 | }; 13 | const res = assign(targ, src); 14 | expect(res).toEqual({ 15 | x: 'X', 16 | y: 'y' 17 | }); 18 | expect(targ).toEqual({ 19 | x: 'x', 20 | y: 'y', 21 | }); 22 | expect(src).toEqual({ 23 | x: 'X', 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/helpers/assign.ts: -------------------------------------------------------------------------------- 1 | export function assign(target: T, source: U): T { 2 | return Object.assign( 3 | {}, 4 | target ? target : {} as T, 5 | source ? source : {} as U 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/helpers/assignDeep.spec.ts: -------------------------------------------------------------------------------- 1 | import { assignDeep } from './'; 2 | 3 | describe('assignDeep', () => { 4 | 5 | it('Functions like assign for top level props', () => { 6 | const targ = { 7 | x: 'x', 8 | y: 'y', 9 | }; 10 | const src = { 11 | x: 'X', 12 | }; 13 | const res = assignDeep(targ, src); 14 | expect(res).toEqual({ 15 | x: 'X', 16 | y: 'y' 17 | }); 18 | expect(targ).toEqual({ 19 | x: 'x', 20 | y: 'y', 21 | }); 22 | expect(src).toEqual({ 23 | x: 'X', 24 | }); 25 | }); 26 | 27 | it('preserves nested props on the target', () => { 28 | const targ = { 29 | nest: { 30 | x: 'x', 31 | y: 'y' 32 | } 33 | }; 34 | const src = { 35 | nest: { 36 | x: 'X' 37 | } 38 | }; 39 | const res = assignDeep(targ, src); 40 | expect(res).toEqual({ 41 | nest: { 42 | x: 'X', 43 | y: 'y' 44 | } 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/app/helpers/assignDeep.ts: -------------------------------------------------------------------------------- 1 | import { assign } from './assign'; 2 | 3 | export function assignDeep(target: T, source: U = {} as any) { 4 | const result = assign(target, source); 5 | // Merge source child objects 6 | for (const key in source) { 7 | if (typeof source[key] === 'object') { 8 | result[key] = assignDeep(target[key] ? target[key] as any : {}, source[key] as any); 9 | } 10 | } 11 | // Clone target child objects not in source 12 | for (const key in target) { 13 | if (!source[key as any] && typeof target[key] === 'object') { 14 | result[key] = assignDeep(target[key] as any, {}); 15 | } 16 | } 17 | return result; 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/helpers/getErrorMessage.spec.ts: -------------------------------------------------------------------------------- 1 | import { tryGetErrorMessage } from './'; 2 | 3 | describe('getErrorMessage', () => { 4 | 5 | describe('tryGetErrorMessage', () => { 6 | 7 | it('Returns th message WHEN it is a string', () => { 8 | expect(tryGetErrorMessage('error')).toBe('error'); 9 | }); 10 | 11 | it('returns the string under a message or error property on the object if there is one', () => { 12 | expect(tryGetErrorMessage({ responseMessage: 'error' })).toBe('error'); 13 | expect(tryGetErrorMessage({ responseError: 'error' })).toBe('error'); 14 | }); 15 | 16 | it(`returns the string under a message or error property 17 | on a child prop the object if there is one`, () => { 18 | expect(tryGetErrorMessage({ prop: { responseMessage: 'error' } })).toBe('error'); 19 | expect(tryGetErrorMessage({ prop: { responseError: 'error' } })).toBe('error'); 20 | }); 21 | 22 | it(`returns falsy when there is no string or message or error props`, () => { 23 | expect(tryGetErrorMessage({ prop: { other: 'error' } })).toBeFalsy(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/helpers/getErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import { IterableChanges } from '@angular/core/core'; 2 | import { Messages } from '../resources/messages'; 3 | 4 | export const tryGetErrorMessage = (error: any): string | void => { 5 | if (error) { 6 | if (typeof error === 'string') { 7 | return error; 8 | } 9 | for (const key in error) { 10 | if (error.hasOwnProperty(key) && 11 | (/message/i.test(key) || /error/i.test(key) || typeof error[key] === 'object')) { 12 | const message = tryGetErrorMessage(error[key]); 13 | if (message) { 14 | return message; 15 | } 16 | } 17 | } 18 | } 19 | }; 20 | 21 | export const getErrorMessage = (error: any, fallback = ''): string => { 22 | const message = tryGetErrorMessage(error); 23 | return message ? message : (fallback ? fallback : Messages.ApiResponse.ServerError); 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/helpers/hashReducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer } from '@ngrx/store'; 2 | import { ActionMap } from './'; 3 | 4 | export function hashReducer(map: ActionMap): ActionReducer { 5 | return (state, action) => map[action.type] ? map[action.type](state, action) : state; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './subscriber.component'; 2 | export * from './assign'; 3 | export * from './getErrorMessage'; 4 | export * from './assignDeep'; 5 | export * from './actionMap'; 6 | export * from './hashReducer'; 7 | export * from './useDefaultState'; 8 | -------------------------------------------------------------------------------- /src/app/helpers/subscriber.component.ts: -------------------------------------------------------------------------------- 1 | import { OnDestroy } from '@angular/core'; 2 | import { Subscription } from 'rxjs/Subscription'; 3 | 4 | export abstract class SubscriberComponent implements OnDestroy { 5 | subscriptions: Subscription[] = []; 6 | 7 | ngOnDestroy() { 8 | this.subscriptions.forEach(s => s.unsubscribe()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/helpers/useDefaultState.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer, Action } from '@ngrx/store'; 2 | 3 | export function useDefaultState(defaultState: T) { 4 | return (reducer: ActionReducer): ActionReducer => (state = defaultState, action: Action) => reducer(state, action); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/landing-page/landing-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hello, world!

4 |

This is a demo app using angular 2 and firebase, as well as ngrx/store.

5 |

See more »

6 |
7 |
8 |
9 | 10 |
11 |
12 |

Heading

13 |

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

14 |

View details »

15 |
16 |
17 |

Heading

18 |

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.

19 |

View details »

20 |
21 |
22 |

Heading

23 |

Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

24 |

View details »

25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /src/app/landing-page/landing-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 3 | 4 | import { LandingPageComponent } from './landing-page.component'; 5 | 6 | describe('LandingPageComponent', () => { 7 | let component: LandingPageComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ LandingPageComponent ], 13 | schemas: [NO_ERRORS_SCHEMA] 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(LandingPageComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/landing-page/landing-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-landing-page', 5 | templateUrl: './landing-page.component.html', 6 | }) 7 | export class LandingPageComponent {} 8 | -------------------------------------------------------------------------------- /src/app/log-in/log-in.component.html: -------------------------------------------------------------------------------- 1 | 3 | 41 | -------------------------------------------------------------------------------- /src/app/log-in/log-in.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FormBuilder } from '@angular/forms'; 3 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 4 | 5 | import { Store } from '@ngrx/store'; 6 | import { emailValid, passwordValid } from '../validators'; 7 | import { AppState } from 'app/store/app.state'; 8 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 9 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 10 | 11 | @Component({ 12 | selector: 'app-log-in', 13 | templateUrl: './log-in.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class LogInComponent { 17 | formState$ = this.state.select(s => s.user.logIn); 18 | 19 | formGroup = new TypedFormGroup({ 20 | email: new TypedFormControl('', emailValid), 21 | password: new TypedFormControl(''), 22 | }); 23 | 24 | constructor(private state: Store) {} 25 | 26 | hideLogInModal() { 27 | this.state.dispatch(new LogInActions.HideModal()); 28 | } 29 | 30 | emailPasswordLogin() { 31 | if (this.formGroup.valid) { 32 | this.state.dispatch( 33 | new LogInActions.LogIn({ 34 | email: this.formGroup.value.email, 35 | password: this.formGroup.value.password, 36 | }) 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/nav/nav.component.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /src/app/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import { NavActions } from 'app/store/nav/nav.actions'; 3 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 4 | 5 | import { Store } from '@ngrx/store'; 6 | import { IsLoggedInGuard } from 'app/shared/guards/isLoggedIn.guard'; 7 | import { AppState } from 'app/store/app.state'; 8 | 9 | declare const window: Window; 10 | 11 | @Component({ 12 | selector: 'app-nav', 13 | templateUrl: './nav.component.html', 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class NavComponent { 17 | userState$ = this.state.select(s => s.user); 18 | navState$ = this.state.select(s => s.nav); 19 | isLoggedIn$ = this.isLoggedInGuard.isLoggedIn(); 20 | window = window; 21 | 22 | constructor( 23 | private state: Store, 24 | private isLoggedInGuard: IsLoggedInGuard 25 | ) {} 26 | 27 | toggleNavigation() { 28 | this.state.dispatch(new NavActions.ToggleNavigation()); 29 | } 30 | 31 | showLogInModal() { 32 | this.state.dispatch(new LogInActions.ShowModal()); 33 | } 34 | 35 | logOut() { 36 | this.state.dispatch(new LogInActions.LogOut()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/reset-password/resetPassword.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Reset your password

3 |
5 |
6 | 7 | 12 | 13 |
14 | 17 | Send reset email 18 | 19 |
20 |
-------------------------------------------------------------------------------- /src/app/reset-password/resetPassword.component.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | import { emailValid } from '../validators'; 3 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 4 | import { AccountAppState } from 'app/account/state/store.config'; 5 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 6 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 7 | import { ResetPasswordAppState } from 'app/reset-password/state/store.config'; 8 | import { ResetPasswordFormActions } from 'app/reset-password/state/form/resetPasswordForm.actions'; 9 | 10 | @Component({ 11 | selector: 'app-password-reset', 12 | templateUrl: './resetPassword.component.html', 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | }) 15 | export class ResetPasswordComponent { 16 | formGroup = new TypedFormGroup({ 17 | email: new TypedFormControl('', emailValid), 18 | }); 19 | 20 | formState$ = this.state.select(s => s.resetPassword.form); 21 | 22 | constructor(private state: Store) {} 23 | 24 | resetPassword() { 25 | if (this.formGroup.valid) { 26 | this.state.dispatch( 27 | new ResetPasswordFormActions.Reset(this.formGroup.value.email) 28 | ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/reset-password/resetPassword.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ResetPasswordComponent } from 'app/reset-password/resetPassword.component'; 3 | import { RouterModule } from '@angular/router'; 4 | import { ResetPasswordStateModule } from 'app/reset-password/state/resetPasswordState.module'; 5 | import { SharedModule } from 'app/shared/shared.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | RouterModule.forChild([ 11 | { path: '', pathMatch: 'full', component: ResetPasswordComponent }, 12 | ]), 13 | ResetPasswordStateModule, 14 | ], 15 | declarations: [ResetPasswordComponent], 16 | }) 17 | export class ResetPasswordModule {} 18 | -------------------------------------------------------------------------------- /src/app/reset-password/state/form/resetPasswordForm.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export enum ResetPasswordFormActionTypes { 2 | Reset = '[ResetPassword] Reset', 3 | Failure = '[ResetPassword] Failure', 4 | Success = '[ResetPassword] Success', 5 | } 6 | -------------------------------------------------------------------------------- /src/app/reset-password/state/form/resetPasswordForm.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { ResetPasswordFormActionTypes } from 'app/reset-password/state/form/resetPasswordForm.actionTypes'; 3 | 4 | export namespace ResetPasswordFormActions { 5 | interface BaseResetPasswordFormAction extends Action { 6 | readonly type: ResetPasswordFormActionTypes; 7 | } 8 | 9 | export class Reset implements BaseResetPasswordFormAction { 10 | readonly type = ResetPasswordFormActionTypes.Reset; 11 | constructor(public payload: string) {} 12 | } 13 | 14 | export class Failure implements BaseResetPasswordFormAction { 15 | readonly type = ResetPasswordFormActionTypes.Failure; 16 | } 17 | 18 | export class Success implements BaseResetPasswordFormAction { 19 | readonly type = ResetPasswordFormActionTypes.Success; 20 | } 21 | 22 | export type ResetPasswordFormAction = Reset | Failure | Success; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/reset-password/state/form/resetPasswordForm.reducer.ts: -------------------------------------------------------------------------------- 1 | import { formReducer } from 'app/store/forms/form.reducer.factory'; 2 | import { ResetPasswordFormActionTypes } from './resetPasswordForm.actionTypes'; 3 | import { FormState } from 'app/store/forms/formState'; 4 | import { Action } from '@ngrx/store'; 5 | 6 | export function resetPasswordFormReducer( 7 | state: FormState, 8 | action: Action 9 | ): FormState { 10 | return formReducer({ 11 | request: ResetPasswordFormActionTypes.Reset, 12 | failure: ResetPasswordFormActionTypes.Failure, 13 | success: ResetPasswordFormActionTypes.Success, 14 | })(state, action); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/reset-password/state/resetPassword.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions } from '@ngrx/effects'; 3 | import { ResetPasswordFormActions } from './form/resetPasswordForm.actions'; 4 | import { ResetPasswordFormActionTypes } from 'app/reset-password/state/form/resetPasswordForm.actionTypes'; 5 | import { Observable } from 'rxjs/Observable'; 6 | import { AngularFireAuth } from 'angularfire2/auth'; 7 | 8 | @Injectable() 9 | export class ResetPasswordEffects { 10 | resetPassword$ = this.actions$ 11 | .ofType( 12 | ResetPasswordFormActionTypes.Reset 13 | ) 14 | .switchMap(action => 15 | Observable.of(this.auth.auth.sendPasswordResetEmail(action.payload)) 16 | .map(() => new ResetPasswordFormActions.Success()) 17 | .catch(() => 18 | Observable.of(new ResetPasswordFormActions.Failure()) 19 | ) 20 | ); 21 | 22 | constructor(private actions$: Actions, private auth: AngularFireAuth) {} 23 | } 24 | -------------------------------------------------------------------------------- /src/app/reset-password/state/resetPasswordState.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { StoreModule } from '@ngrx/store'; 3 | import { 4 | RESET_PASSWORD_STORE_KEY, 5 | resetPasswordReducers, 6 | } from 'app/reset-password/state/store.config'; 7 | import { EffectsModule } from '@ngrx/effects'; 8 | import { ResetPasswordEffects } from 'app/reset-password/state/resetPassword.effects'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | StoreModule.forFeature(RESET_PASSWORD_STORE_KEY, resetPasswordReducers), 13 | EffectsModule.forFeature([ResetPasswordEffects]), 14 | ], 15 | }) 16 | export class ResetPasswordStateModule {} 17 | -------------------------------------------------------------------------------- /src/app/reset-password/state/store.config.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from 'app/store/forms/formState'; 2 | import { AppFeatureState } from 'app/store/utils/featureState'; 3 | import { AppState } from 'app/store/app.state'; 4 | import { ActionReducerMap } from '@ngrx/store'; 5 | import { resetPasswordFormReducer } from 'app/reset-password/state/form/resetPasswordForm.reducer'; 6 | 7 | export const RESET_PASSWORD_STORE_KEY = 'resetPassword'; 8 | 9 | export interface ResetPasswordFeatureState { 10 | form: FormState; 11 | } 12 | 13 | export type ResetPasswordAppState = AppFeatureState< 14 | AppState, 15 | typeof RESET_PASSWORD_STORE_KEY, 16 | ResetPasswordFeatureState 17 | >; 18 | 19 | export const resetPasswordReducers: ActionReducerMap< 20 | ResetPasswordFeatureState 21 | > = { 22 | form: resetPasswordFormReducer, 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/resources/messages.ts: -------------------------------------------------------------------------------- 1 | export const Messages = { 2 | Validation: { 3 | PasswordsNotEqual: 'Passwords are not equal', 4 | EmailInvalid: 'Please enter a valid email address', 5 | PasswordInvalid: 'Password must be at least 6 characters long and contain at least one number', 6 | TodoNameMinLength: 'Please enter a name for this task' 7 | }, 8 | ApiResponse: { 9 | ServerError: 'An error occurred. Please try again.', 10 | UpdatePasswordSuccess: 'Password updated successfully.', 11 | UpdateEmailSuccess: 'Email updated successfully.', 12 | UpdatePhotoUrlSucess: 'Profile picture updated sucessfully', 13 | SendVerificationEmailSuccess: 'Verification email sent.' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/routing/appRouting.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { LandingPageComponent } from 'app/landing-page/landing-page.component'; 4 | import { IsLoggedInGuard } from 'app/shared/guards/isLoggedIn.guard'; 5 | import { IsNotLoggedInGuard } from 'app/shared/guards/isNotLoggedIn.guard'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | RouterModule.forRoot([ 10 | { path: '', component: LandingPageComponent }, 11 | { 12 | path: 'sign-up', 13 | loadChildren: '../sign-up/signUp.module#SignUpModule', 14 | }, 15 | { 16 | path: 'reset-password', 17 | loadChildren: 18 | '../reset-password/resetPassword.module#ResetPasswordModule', 19 | canActivate: [IsNotLoggedInGuard], 20 | }, 21 | { 22 | path: 'account', 23 | loadChildren: '../account/account.module#AccountModule', 24 | canActivate: [IsLoggedInGuard], 25 | }, 26 | { path: '**', redirectTo: '' }, 27 | ]), 28 | ], 29 | exports: [RouterModule], 30 | }) 31 | export class AppRoutingModule {} 32 | -------------------------------------------------------------------------------- /src/app/shared/forms/showErrors.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; 2 | 3 | export function showErrors(control: AbstractControl) { 4 | if ((control as FormGroup | FormArray).controls) { 5 | // tslint:disable-next-line:forin 6 | for (const name in (control as FormGroup | FormArray).controls) { 7 | showErrors((control as FormGroup).controls[name]); 8 | } 9 | } 10 | control.markAsTouched({ onlySelf: true }); 11 | control.updateValueAndValidity({ onlySelf: true }); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/forms/typedAbstractControl.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '@angular/forms'; 2 | import { 3 | TypedValidatorFn, 4 | TypedAsyncValidatorFn, 5 | } from 'app/shared/forms/typedValidatorFn'; 6 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 7 | import { TypedFormArray } from 'app/shared/forms/typedFormArray'; 8 | import { ValidationErrors } from '@angular/forms'; 9 | import { Observable } from 'rxjs/Observable'; 10 | 11 | export interface TypedAbstractControl< 12 | TValue, 13 | TErrors extends ValidationErrors = any 14 | > extends AbstractControl { 15 | validator: TypedValidatorFn | null; 16 | asyncValidator: TypedAsyncValidatorFn | null; 17 | errors: TErrors; 18 | readonly value: TValue; 19 | readonly valueChanges: Observable; 20 | setValidators( 21 | newValidator: 22 | | TypedValidatorFn 23 | | Array>> 24 | | null 25 | ): void; 26 | setAsyncValidators( 27 | newValidator: 28 | | TypedAsyncValidatorFn 29 | | Array>> 30 | | null 31 | ): void; 32 | setValue(value: TValue, options?: Object): void; 33 | patchValue(value: TValue, options?: Object): void; 34 | reset(value?: TValue, options?: Object): void; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/shared/forms/typedFormArray.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormArray, 4 | AbstractControl, 5 | ValidatorFn, 6 | AsyncValidatorFn, 7 | } from '@angular/forms'; 8 | import { Observable } from 'rxjs/Observable'; 9 | import { TypedAbstractControl } from 'app/shared/forms/typedAbstractControl'; 10 | import { ValidationErrors } from '@angular/forms'; 11 | import { 12 | TypedValidatorFn, 13 | TypedAsyncValidatorFn, 14 | } from 'app/shared/forms/typedValidatorFn'; 15 | 16 | export class TypedFormArray< 17 | T, 18 | U extends ValidationErrors = any 19 | > extends FormArray implements TypedAbstractControl { 20 | readonly value: T[]; 21 | readonly validator: TypedValidatorFn; 22 | readonly asyncValidator: TypedAsyncValidatorFn; 23 | readonly errors: U; 24 | 25 | constructor( 26 | public readonly controls: TypedAbstractControl[], 27 | validator?: 28 | | TypedValidatorFn 29 | | Array>>, 30 | asyncValidator?: 31 | | TypedAsyncValidatorFn 32 | | Array>> 33 | ) { 34 | super(controls, validator, asyncValidator); 35 | } 36 | 37 | at(index: number): TypedAbstractControl { 38 | return super.at(index); 39 | } 40 | push(control: TypedAbstractControl) { 41 | super.push(control); 42 | } 43 | insert(index: number, control: TypedAbstractControl) { 44 | super.insert(index, control); 45 | } 46 | setControl(index: number, control: TypedAbstractControl) { 47 | super.setControl(index, control); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/forms/typedFormControl.ts: -------------------------------------------------------------------------------- 1 | import { FormControl, ValidatorFn, AsyncValidatorFn } from '@angular/forms'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { TypedAbstractControl } from 'app/shared/forms/typedAbstractControl'; 4 | import { 5 | TypedValidatorFn, 6 | TypedAsyncValidatorFn, 7 | } from 'app/shared/forms/typedValidatorFn'; 8 | import { ValidationErrors } from '@angular/forms'; 9 | 10 | export class TypedFormControl< 11 | T, 12 | U extends ValidationErrors = any 13 | > extends FormControl implements TypedAbstractControl { 14 | readonly value: T; 15 | readonly validator: TypedValidatorFn; 16 | readonly asyncValidator: TypedAsyncValidatorFn; 17 | readonly errors: U; 18 | 19 | constructor( 20 | formState?: T | { value?: T; disabled?: boolean }, 21 | validator?: 22 | | TypedValidatorFn 23 | | Array>>, 24 | asyncValidator?: 25 | | TypedAsyncValidatorFn 26 | | Array>> 27 | ) { 28 | super(formState, validator, asyncValidator); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/forms/typedFormGroup.ts: -------------------------------------------------------------------------------- 1 | import { scrollToElement } from '../utils/scrollToElement'; 2 | import { 3 | AbstractControl, 4 | AsyncValidatorFn, 5 | FormGroup, 6 | ValidatorFn, 7 | } from '@angular/forms'; 8 | 9 | import { showErrors } from './showErrors'; 10 | import { TypedAbstractControl } from 'app/shared/forms/typedAbstractControl'; 11 | import { ValidationErrors } from '@angular/forms'; 12 | import { 13 | TypedValidatorFn, 14 | TypedAsyncValidatorFn, 15 | } from 'app/shared/forms/typedValidatorFn'; 16 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 17 | import { MapLike } from 'typescript'; 18 | 19 | export class TypedFormGroup< 20 | T extends MapLike, 21 | U extends ValidationErrors = any 22 | > extends FormGroup implements TypedAbstractControl { 23 | readonly value: T; 24 | readonly validator: TypedValidatorFn; 25 | readonly asyncValidator: TypedAsyncValidatorFn; 26 | readonly errors: U; 27 | constructor( 28 | public readonly controls: { 29 | [key in keyof T]: TypedFormControl 30 | }, 31 | validator?: TypedValidatorFn | TypedValidatorFn>[], 32 | asyncValidator?: 33 | | TypedAsyncValidatorFn 34 | | TypedAsyncValidatorFn>[] 35 | ) { 36 | super(controls, validator, asyncValidator); 37 | } 38 | 39 | showErrors() { 40 | showErrors(this); 41 | setTimeout(() => 42 | scrollToElement( 43 | '[formControl].ng-invalid.ng-touched,[formControlName].ng-invalid.ng-touched' 44 | ) 45 | ); 46 | return this.valid; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/shared/forms/typedValidatorFn.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorFn, ValidationErrors } from '@angular/forms'; 2 | import { TypedAbstractControl } from 'app/shared/forms/typedAbstractControl'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | export type TypedValidatorFn = ( 6 | control: TypedAbstractControl 7 | ) => U | null; 8 | 9 | export type TypedAsyncValidatorFn = ( 10 | control: TypedAbstractControl 11 | ) => Observable | Promise; 12 | -------------------------------------------------------------------------------- /src/app/shared/google/google-plus-auth-button.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import * as firebase from 'firebase/app'; 3 | 4 | import { Store } from '@ngrx/store'; 5 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 6 | import { AccountAppState } from 'app/account/state/store.config'; 7 | import { ChangeDetectionStrategy } from '@angular/core'; 8 | 9 | @Component({ 10 | selector: 'app-google-plus-auth-button', 11 | template: ` 12 | 13 | 14 | Sign in with Google 15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class GooglePlusAuthButtonComponent { 19 | constructor(private state: Store) {} 20 | 21 | login() { 22 | this.state.dispatch( 23 | new LogInActions.LogIn(new firebase.auth.GoogleAuthProvider()) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/guards/isLoggedIn.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, CanActivateChild } from '@angular/router'; 3 | import { AngularFireAuth } from 'angularfire2/auth'; 4 | 5 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 6 | import { Store } from '@ngrx/store'; 7 | import { AppState } from 'app/store/app.state'; 8 | 9 | @Injectable() 10 | export class IsLoggedInGuard implements CanActivate, CanActivateChild { 11 | constructor( 12 | private auth: AngularFireAuth, 13 | private state: Store 14 | ) {} 15 | 16 | canActivate() { 17 | return this.redirectIfNotLoggedIn(); 18 | } 19 | 20 | canActivateChild() { 21 | return this.redirectIfNotLoggedIn(); 22 | } 23 | 24 | redirectIfNotLoggedIn() { 25 | return this.isLoggedIn().map(isAuth => { 26 | if (!isAuth) { 27 | this.state.dispatch(new LogInActions.ShowModal()); 28 | } 29 | return isAuth; 30 | }); 31 | } 32 | 33 | continueIfLoggedIn() { 34 | return this.auth.authState.filter(a => !!a); 35 | } 36 | 37 | isLoggedIn() { 38 | return this.auth.authState.map(a => !!a); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/shared/guards/isNotLoggedIn.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, CanActivateChild } from '@angular/router'; 2 | import { IsLoggedInGuard } from './isLoggedIn.guard'; 3 | import { Injectable } from '@angular/core'; 4 | 5 | @Injectable() 6 | export class IsNotLoggedInGuard implements CanActivate, CanActivateChild { 7 | 8 | constructor( 9 | private isLoggedInGuard: IsLoggedInGuard 10 | ) {} 11 | 12 | canActivate() { 13 | return this.isLoggedInGuard.isLoggedIn().map(a => !a); 14 | } 15 | 16 | canActivateChild() { 17 | return this.isLoggedInGuard.isLoggedIn().map(a => !a); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/modal/modal.component.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/app/shared/modal/modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | // import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | // import { ModalComponent } from './modal.component'; 3 | 4 | // describe('ModalComponent', () => { 5 | // let component: ModalComponent; 6 | // let fixture: ComponentFixture; 7 | 8 | // beforeEach(async(() => { 9 | // TestBed.configureTestingModule({ 10 | // declarations: [ModalComponent] 11 | // }) 12 | // .compileComponents(); 13 | // })); 14 | 15 | // beforeEach(() => { 16 | // fixture = TestBed.createComponent(ModalComponent); 17 | // component = fixture.componentInstance; 18 | // fixture.detectChanges(); 19 | // }); 20 | 21 | // it('should emit a close attempt event WHEN a click is intercepted on the backdrop element', () => { 22 | // spyOn(fixture.componentInstance.cancel, 'emit'); 23 | // (fixture.nativeElement as HTMLElement) 24 | // .querySelector('.modal-open') 25 | // .dispatchEvent(new MouseEvent('click')); 26 | // expect(fixture.componentInstance.cancel.emit) 27 | // .toHaveBeenCalled(); 28 | // }); 29 | 30 | // it('should emit a close attempt event WHEN an escape is intercepted on the backdrop element', () => { 31 | // spyOn(fixture.componentInstance.cancel, 'emit'); 32 | // (fixture.nativeElement as HTMLElement) 33 | // .dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape', bubbles: true })); 34 | // expect(fixture.componentInstance.cancel.emit) 35 | // .toHaveBeenCalled(); 36 | // }); 37 | 38 | // it('should emit a close attempt event WHEN an enter is intercepted on the backdrop element', () => { 39 | // spyOn(fixture.componentInstance.cancel, 'emit'); 40 | // (fixture.nativeElement as HTMLElement) 41 | // .dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab', bubbles: true })); 42 | // expect(fixture.componentInstance.cancel.emit) 43 | // .toHaveBeenCalled(); 44 | // }); 45 | // }); 46 | -------------------------------------------------------------------------------- /src/app/shared/modal/modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, HostListener, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-modal', 5 | templateUrl: './modal.component.html' 6 | }) 7 | export class ModalComponent { 8 | 9 | @Output() cancel = new EventEmitter(); 10 | 11 | @HostListener('document:keydown', ['$event']) 12 | onEscape(event: KeyboardEvent) { 13 | if (event.code === 'Escape' || event.code === 'Tab') { 14 | this.onCancel(); 15 | } 16 | } 17 | 18 | onCancel() { 19 | this.cancel.emit(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/shared/panel/panel.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 | 7 |
8 |
-------------------------------------------------------------------------------- /src/app/shared/panel/panel.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-panel', 5 | templateUrl: 'panel.component.html', 6 | changeDetection: ChangeDetectionStrategy.OnPush 7 | }) 8 | 9 | export class PanelComponent { 10 | @Input() state = 'default'; 11 | @Input() title: string | undefined; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { GooglePlusAuthButtonComponent } from './google/google-plus-auth-button'; 2 | import { ModalComponent } from './modal/modal.component'; 3 | import { PanelComponent } from './panel/panel.component'; 4 | import { SubmitButtonComponent } from './submit-button/submit-button.component'; 5 | import { ValidationMessageComponent } from './validation-message/validation-message.component'; 6 | import { CommonModule } from '@angular/common'; 7 | import { NgModule } from '@angular/core'; 8 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 9 | import { HttpModule } from '@angular/http'; 10 | import { RouterModule } from '@angular/router'; 11 | import { IsLoggedInGuard } from 'app/shared/guards/isLoggedIn.guard'; 12 | import { IsNotLoggedInGuard } from 'app/shared/guards/isNotLoggedIn.guard'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, 17 | FormsModule, 18 | ReactiveFormsModule, 19 | HttpModule, 20 | RouterModule, 21 | ], 22 | declarations: [ 23 | ModalComponent, 24 | ValidationMessageComponent, 25 | PanelComponent, 26 | SubmitButtonComponent, 27 | GooglePlusAuthButtonComponent, 28 | ], 29 | exports: [ 30 | CommonModule, 31 | FormsModule, 32 | ReactiveFormsModule, 33 | HttpModule, 34 | ModalComponent, 35 | ValidationMessageComponent, 36 | PanelComponent, 37 | SubmitButtonComponent, 38 | GooglePlusAuthButtonComponent, 39 | ], 40 | providers: [IsLoggedInGuard, IsNotLoggedInGuard], 41 | }) 42 | export class SharedModule {} 43 | -------------------------------------------------------------------------------- /src/app/shared/submit-button/submit-button.component.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/app/shared/submit-button/submit-button.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-submit-button', 5 | changeDetection: ChangeDetectionStrategy.OnPush, 6 | templateUrl: './submit-button.component.html' 7 | }) 8 | 9 | export class SubmitButtonComponent { 10 | @Input() requesting = false; 11 | @Input() disabled = false; 12 | @Input() buttonState = 'default'; 13 | @Input() extraClasses = ''; 14 | @Input() requestingText = ''; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/utils/scrollToElement.ts: -------------------------------------------------------------------------------- 1 | export function scrollToElement(selector: string) { 2 | let el = document.querySelector(selector) as HTMLElement | null; 3 | while (el && window.getComputedStyle(el).display === 'none') { 4 | el = el.parentElement; 5 | } 6 | if (el) { 7 | el.scrollIntoView({ behavior: 'smooth' }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/validation-message/validation-message.component.ts: -------------------------------------------------------------------------------- 1 | import { getFile } from 'ts-node/dist'; 2 | import { error } from 'util'; 3 | import { FormControl, FormGroup } from '@angular/forms'; 4 | import { Component, Input } from '@angular/core'; 5 | 6 | /** 7 | * Finds and displays the error message associated with a given form control or group. 8 | * 9 | * Rules: 10 | * - Won't display mesages for controls that have not been touched 11 | * - Will only ever display one message at a time 12 | */ 13 | @Component({ 14 | selector: 'app-validation-message', 15 | template: `{{ message }}` 16 | }) 17 | export class ValidationMessageComponent { 18 | 19 | @Input() control: FormControl | FormGroup; 20 | 21 | get message() { 22 | return this.getMessage(this.control); 23 | } 24 | 25 | private getMessage(control: FormControl | FormGroup, isChild: boolean = false): string | void { 26 | if (control && control.touched || isChild) { 27 | if (control.errors) { 28 | for (const errorName in control.errors) { 29 | if (control.errors.hasOwnProperty(errorName)) { 30 | return control.errors[errorName]; 31 | } 32 | } 33 | } 34 | const fg = control as FormGroup; 35 | if (fg.controls) { 36 | for (const controlName in fg.controls) { 37 | if (fg.controls.hasOwnProperty(controlName)) { 38 | const error = this.getMessage(fg[controlName]); 39 | if (error) { 40 | return error; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/sign-up/signUp.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Sign Up

3 |
4 | 5 |
6 |
7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 | {{ (formState$ | async)?.failureMessage }} 24 | 25 | Sign Up 26 | 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/app/sign-up/signUp.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FormBuilder } from '@angular/forms'; 3 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 4 | 5 | import { Messages } from '../resources/messages'; 6 | import { Store } from '@ngrx/store'; 7 | import { emailValid, passwordValid } from '../validators'; 8 | import { AppState } from 'app/store/app.state'; 9 | import { TypedFormGroup } from 'app/shared/forms/typedFormGroup'; 10 | import { TypedFormControl } from 'app/shared/forms/typedFormControl'; 11 | import { SignUpAppState } from 'app/sign-up/state/store.config'; 12 | import { SignUpActions } from 'app/sign-up/state/form/signUpForm.actions'; 13 | import { valuesEqual } from 'app/validators/valuesEqual'; 14 | 15 | @Component({ 16 | templateUrl: './signUp.component.html', 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | }) 19 | export class SignUpComponent { 20 | form = new TypedFormGroup( 21 | { 22 | email: new TypedFormControl('', emailValid), 23 | password: new TypedFormControl('', passwordValid), 24 | confirmPassword: new TypedFormControl(''), 25 | }, 26 | valuesEqual( 27 | () => this.form.controls.password, 28 | () => this.form.controls.confirmPassword 29 | )() 30 | ); 31 | 32 | formState$ = this.state.select(s => s.signUp.form); 33 | 34 | constructor(private state: Store) {} 35 | 36 | signUp() { 37 | if (this.form.valid) { 38 | this.state.dispatch( 39 | new SignUpActions.SignUp({ 40 | email: this.form.value.email, 41 | password: this.form.value.password, 42 | }) 43 | ); 44 | } 45 | } 46 | 47 | showLogin() { 48 | this.state.dispatch(new LogInActions.ShowModal()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/sign-up/signUp.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { SharedModule } from 'app/shared/shared.module'; 4 | import { SignUpComponent } from 'app/sign-up/signUp.component'; 5 | import { SignUpStateModule } from 'app/sign-up/state/signUp.state.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | SignUpStateModule, 11 | RouterModule.forChild([ 12 | { path: '', pathMatch: 'full', component: SignUpComponent }, 13 | ]), 14 | ], 15 | declarations: [SignUpComponent], 16 | }) 17 | export class SignUpModule {} 18 | -------------------------------------------------------------------------------- /src/app/sign-up/state/form/signUpForm.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace SignUpFormActionTypes { 2 | export const SignUp = '[SignUp] Sign Up'; 3 | export const Failure = '[SignUp] Sign Up Failure'; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/sign-up/state/form/signUpForm.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { SignUpFormActionTypes } from './signUpForm.actionTypes'; 4 | 5 | export interface EmailPasswordCredentials { 6 | readonly email: string; 7 | readonly password: string; 8 | } 9 | 10 | export namespace SignUpActions { 11 | export class SignUp implements Action { 12 | readonly type = SignUpFormActionTypes.SignUp; 13 | constructor(public readonly payload: EmailPasswordCredentials) {} 14 | } 15 | 16 | export class Failure implements Action { 17 | readonly type = SignUpFormActionTypes.Failure; 18 | constructor(public readonly payload: any) {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/sign-up/state/form/signUpForm.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Effect, Actions } from '@ngrx/effects'; 3 | import { AngularFireAuth } from 'angularfire2/auth'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { Store } from '@ngrx/store'; 7 | import { SignUpActions } from './signUpForm.actions'; 8 | import { AppState } from 'app/store/app.state'; 9 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 10 | import { SignUpFormActionTypes } from 'app/sign-up/state/form/signUpForm.actionTypes'; 11 | 12 | @Injectable() 13 | export class SignUpFormEffects { 14 | @Effect() 15 | signUp$ = this.actions$ 16 | .ofType(SignUpFormActionTypes.SignUp) 17 | .switchMap((action: SignUpActions.SignUp) => 18 | Observable.from( 19 | this.auth.auth.createUserWithEmailAndPassword( 20 | action.payload.email, 21 | action.payload.password 22 | ) 23 | ) 24 | .switchMap(authState => 25 | this.auth.authState.map(a => { 26 | a.sendEmailVerification(); 27 | return new LogInActions.Success(); 28 | }) 29 | ) 30 | .catch(error => Observable.of(new SignUpActions.Failure(error))) 31 | ); 32 | 33 | constructor( 34 | private actions$: Actions, 35 | private state: Store, 36 | private auth: AngularFireAuth 37 | ) {} 38 | } 39 | -------------------------------------------------------------------------------- /src/app/sign-up/state/form/signUpForm.reducer.ts: -------------------------------------------------------------------------------- 1 | import { formReducer } from 'app/store/forms/form.reducer.factory'; 2 | 3 | import { SignUpFormActionTypes } from './signUpForm.actionTypes'; 4 | import { FormState } from 'app/store/forms/formState'; 5 | import { Action } from '@ngrx/store'; 6 | 7 | export function signUpFormReducer(state: FormState, action: Action): FormState { 8 | return formReducer({ 9 | request: SignUpFormActionTypes.SignUp, 10 | failure: SignUpFormActionTypes.Failure, 11 | })(state, action); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/sign-up/state/signUp.state.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { StoreModule } from '@ngrx/store'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | import { SignUpFormEffects } from 'app/sign-up/state/form/signUpForm.effects'; 5 | import { 6 | SIGN_UP_STORE_KEY, 7 | SignUpReducers, 8 | } from 'app/sign-up/state/store.config'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | StoreModule.forFeature(SIGN_UP_STORE_KEY, SignUpReducers), 13 | EffectsModule.forFeature([SignUpFormEffects]), 14 | ], 15 | }) 16 | export class SignUpStateModule {} 17 | -------------------------------------------------------------------------------- /src/app/sign-up/state/store.config.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from 'app/store/forms/formState'; 2 | import { AppState } from 'app/store/app.state'; 3 | import { AppFeatureState } from 'app/store/utils/featureState'; 4 | import { ActionReducerMap } from '@ngrx/store'; 5 | import { signUpFormReducer } from 'app/sign-up/state/form/signUpForm.reducer'; 6 | 7 | export const SIGN_UP_STORE_KEY = 'signUp'; 8 | 9 | export interface SignUpFeatureState { 10 | form: FormState; 11 | } 12 | 13 | export type SignUpAppState = AppFeatureState< 14 | AppState, 15 | typeof SIGN_UP_STORE_KEY, 16 | SignUpFeatureState 17 | >; 18 | 19 | export const SignUpReducers: ActionReducerMap = { 20 | form: signUpFormReducer, 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/store/app.state.ts: -------------------------------------------------------------------------------- 1 | import { DefaultNavState, NavState } from 'app/store/nav/nav.state'; 2 | import { defaultUserState, UserState } from 'app/store/user/user.state'; 3 | 4 | export interface AppState { 5 | user: UserState; 6 | nav: NavState; 7 | } 8 | 9 | export const defaultAppState: AppState = { 10 | user: defaultUserState, 11 | nav: DefaultNavState, 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/store/forms/form.reducer.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { assign, assignDeep, getErrorMessage } from '../../helpers'; 4 | import { shouldNotAlterStateOnUnknownAction } from '../testing'; 5 | import { FormReducerOptions, formReducer } from 'app/store/forms/form.reducer.factory'; 6 | import { FormState, FormStates } from 'app/store/forms/formState'; 7 | 8 | describe('Form reducer factory', () => { 9 | 10 | const config: FormReducerOptions = { 11 | show: 'show', 12 | hide: 'hide', 13 | toggle: 'toggle', 14 | request: 'request', 15 | success: 'success', 16 | failure: 'failure', 17 | extras: [{ 18 | types: 'setExtraProp', 19 | func: (state, action) => assign(state, { extraProp: (action as any).payload }) 20 | }], 21 | successMessage: 'success!' 22 | }; 23 | 24 | interface ExtendedFormState extends FormState { 25 | extraProp: any; 26 | } 27 | 28 | const reducer = formReducer(config); 29 | 30 | let oldState: ExtendedFormState; 31 | 32 | beforeEach(() => { 33 | oldState = assignDeep(FormStates.Default as any, { extraProp: 'hello' }); 34 | }); 35 | 36 | shouldNotAlterStateOnUnknownAction(reducer); 37 | 38 | it('applies the form visibility WHEN show is called', () => { 39 | oldState.showForm = false; 40 | const newState = reducer(oldState, action(config.show as string)); 41 | expect(newState).toEqual(assign(oldState, { showForm: true })); 42 | }); 43 | 44 | it('applies the form visibility WHEN hide is called', () => { 45 | oldState.showForm = true; 46 | const newState = reducer(oldState, action(config.hide as string)); 47 | expect(newState).toEqual(assign(oldState, { showForm: false })); 48 | }); 49 | 50 | it('applies the form visibility WHEN toggle is called', () => { 51 | oldState.showForm = false; 52 | let newState = reducer(oldState, action(config.toggle as string)); 53 | expect(newState).toEqual(assign(oldState, { showForm: true })); 54 | newState = reducer(newState, action(config.toggle as string)); 55 | expect(newState).toEqual(assign(oldState, { showForm: false })); 56 | }); 57 | 58 | it('applies the requesting state WHEN request is called', () => { 59 | const newState = reducer(oldState, action(config.request as string)); 60 | expect(newState).toEqual(assign(oldState, FormStates.Requesting)); 61 | }); 62 | 63 | it('applies the success state WHEN success is called', () => { 64 | const newState = reducer(oldState, action(config.success as string)); 65 | expect(newState).toEqual(assign(oldState, FormStates.Success(config.successMessage))); 66 | }); 67 | 68 | it('applies the failure state WHEN failure is called', () => { 69 | const newState = reducer(oldState, action(config.failure as string)); 70 | expect(newState).toEqual(assign(oldState, FormStates.Failure(getErrorMessage(undefined)))); 71 | }); 72 | 73 | it('Runs custom cases WHEN they are passed', () => { 74 | const newState = reducer(oldState, action('setExtraProp', '1234')); 75 | expect(newState).toEqual(assign(oldState, { extraProp: '1234' })); 76 | }); 77 | }); 78 | 79 | function action(type: string, payload = undefined): Action & { payload: any } { 80 | return { 81 | type: type, 82 | payload: payload 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/app/store/forms/form.reducer.factory.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer, compose } from '@ngrx/store'; 2 | import { FormState, FormStates } from 'app/store/forms/formState'; 3 | 4 | import { ActionMap, assign, getErrorMessage, hashReducer } from '../../helpers'; 5 | import { useDefaultState } from '../../helpers/useDefaultState'; 6 | 7 | type MaybeCollection = T | T[]; 8 | 9 | export interface ActionCaseInput { 10 | types: MaybeCollection; 11 | func: ActionReducer; 12 | } 13 | 14 | export interface ActionCase { 15 | types: string[]; 16 | func: ActionReducer; 17 | } 18 | 19 | export interface FormReducerOptions { 20 | show?: MaybeCollection; 21 | hide?: MaybeCollection; 22 | toggle?: MaybeCollection; 23 | request?: MaybeCollection; 24 | success?: MaybeCollection; 25 | failure?: MaybeCollection; 26 | extras?: ActionCaseInput[]; 27 | successMessage?: string; 28 | failureMessage?: string; 29 | defaultState?: T; 30 | } 31 | 32 | export function formReducer( 33 | config: FormReducerOptions 34 | ): ActionReducer { 35 | const actionMap: ActionMap = {}; 36 | 37 | function addCase( 38 | types: string | string[] | undefined, 39 | func: ActionReducer 40 | ) { 41 | if (types == null) { 42 | return; 43 | } 44 | if (Array.isArray(types)) { 45 | types.forEach(type => (actionMap[type] = func)); 46 | } else { 47 | actionMap[types] = func; 48 | } 49 | } 50 | 51 | addCase(config.show, (state, action) => assign(state, { showForm: true })); 52 | addCase(config.hide, (state, action) => assign(state, { showForm: false })); 53 | addCase(config.toggle, (state, action) => 54 | assign(state, { showForm: !state.showForm }) 55 | ); 56 | addCase(config.request, (state, action) => 57 | assign(state, FormStates.Requesting) 58 | ); 59 | addCase(config.success, (state, action) => 60 | assign(state, FormStates.Success(config.successMessage)) 61 | ); 62 | addCase(config.failure, (state, action) => 63 | assign( 64 | state, 65 | FormStates.Failure( 66 | getErrorMessage(action.payload, config.failureMessage) 67 | ) 68 | ) 69 | ); 70 | 71 | if (config.extras) { 72 | config.extras.forEach(c => addCase(c.types, c.func)); 73 | } 74 | 75 | const defaultState = config.defaultState 76 | ? config.defaultState 77 | : FormStates.Default; 78 | 79 | return compose(useDefaultState(defaultState), hashReducer)(actionMap); 80 | } 81 | -------------------------------------------------------------------------------- /src/app/store/forms/formState.ts: -------------------------------------------------------------------------------- 1 | export interface FormState { 2 | showForm: boolean; 3 | isRequesting: boolean; 4 | successMessage: string; 5 | failureMessage: string; 6 | } 7 | 8 | export namespace FormStates { 9 | 10 | export const Default: FormState = { 11 | showForm: false, 12 | isRequesting: false, 13 | successMessage: '', 14 | failureMessage: '' 15 | }; 16 | 17 | export const Requesting: FormState = { 18 | showForm: true, 19 | isRequesting: true, 20 | successMessage: '', 21 | failureMessage: '' 22 | }; 23 | 24 | export const Success = (message = ''): FormState => { 25 | return { 26 | showForm: true, 27 | isRequesting: false, 28 | successMessage: message, 29 | failureMessage: '' 30 | }; 31 | }; 32 | 33 | export const Failure = (message: string): FormState => { 34 | return { 35 | showForm: true, 36 | isRequesting: false, 37 | successMessage: '', 38 | failureMessage: message 39 | }; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/store/global/global.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace GlobalActionTypes { 2 | export const AppStart = '[Global] App Start'; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/store/global/global.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { GlobalActionTypes } from './global.actionTypes'; 4 | 5 | export namespace GlobalActions { 6 | 7 | export class AppStart implements Action { 8 | readonly type = GlobalActionTypes.AppStart; 9 | payload: void; 10 | } 11 | 12 | export type GlobalAction = AppStart; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/store/global/global.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { GlobalActions } from 'app/store/global/global.actions'; 2 | import { globalReducer } from 'app/store/global/global.reducer'; 3 | import { shouldNotAlterStateOnUnknownAction } from 'app/store/testing'; 4 | 5 | import { assignDeep } from '../../helpers'; 6 | import { AppState, defaultAppState } from '../app.state'; 7 | import { FormStates } from '../forms/formState'; 8 | import { 9 | UserAppState, 10 | initialUserState, 11 | } from 'app/account/user/state/store.config'; 12 | 13 | describe('Global Reducer', () => { 14 | const reducer = globalReducer(state => state); 15 | 16 | let oldState: AppState; 17 | 18 | beforeEach(() => { 19 | oldState = assignDeep(defaultAppState); 20 | }); 21 | 22 | shouldNotAlterStateOnUnknownAction(reducer); 23 | 24 | it(`Assigns missing properties to the state tree 25 | ON App Start 26 | using the default state`, () => { 27 | oldState.user.logIn = undefined; 28 | const newState = reducer(oldState, new GlobalActions.AppStart()); 29 | 30 | expect(newState).toEqual(defaultAppState); 31 | }); 32 | 33 | it(`Does not alter existing state properties 34 | IF they are part of the correct schema`, () => { 35 | oldState.user.logIn.failureMessage = 'Example'; 36 | 37 | const newState = reducer(oldState, new GlobalActions.AppStart()); 38 | 39 | expect(newState.user.logIn.failureMessage).toBe('Example'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/app/store/global/global.reducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer } from '@ngrx/store'; 2 | import { AppState, defaultAppState } from 'app/store/app.state'; 3 | import { GlobalActions } from 'app/store/global/global.actions'; 4 | 5 | import { assignDeep } from '../../helpers/'; 6 | import { GlobalActionTypes } from './global.actionTypes'; 7 | import { environment } from 'environments/environment.prod'; 8 | 9 | export function globalReducer( 10 | reducer: ActionReducer 11 | ): ActionReducer { 12 | return function( 13 | state = defaultAppState, 14 | action: GlobalActions.GlobalAction 15 | ) { 16 | if (!environment.production) { 17 | console.info(action.type, action.payload); 18 | } 19 | 20 | switch (action.type) { 21 | case GlobalActionTypes.AppStart: 22 | state = assignDeep(defaultAppState, state); 23 | } 24 | 25 | return reducer(state, action); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/store/nav/nav.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace NavActionTypes { 2 | export const ToggleNavigation = '[Nav] Toggle Navigation'; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/store/nav/nav.actions.ts: -------------------------------------------------------------------------------- 1 | import { NavActionTypes } from './nav.actionTypes'; 2 | import { Action } from '@ngrx/store'; 3 | export namespace NavActions { 4 | 5 | export class ToggleNavigation implements Action { 6 | readonly type = NavActionTypes.ToggleNavigation; 7 | payload: void; 8 | } 9 | 10 | export type NavAction = 11 | ToggleNavigation; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/store/nav/nav.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { assignDeep } from '../../helpers/assignDeep'; 2 | import { NavReducer } from './nav.reducer'; 3 | import { shouldNotAlterStateOnUnknownAction } from '../testing/reducerTestHelpers'; 4 | import { NavState, DefaultNavState } from 'app/store/nav/nav.state'; 5 | import { NavActions } from 'app/store/nav/nav.actions'; 6 | 7 | describe('Nav Reducer', () => { 8 | 9 | shouldNotAlterStateOnUnknownAction(NavReducer); 10 | 11 | let oldState: NavState; 12 | 13 | beforeEach(() => { 14 | oldState = assignDeep(DefaultNavState); 15 | }); 16 | 17 | it('Reverses the nav visibility WHEN Toggle is called', () => { 18 | let newState = NavReducer(oldState, new NavActions.ToggleNavigation()); 19 | expect(newState.showNavigation).toBe(true); 20 | 21 | newState = NavReducer(newState, new NavActions.ToggleNavigation()); 22 | expect(newState.showNavigation).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/store/nav/nav.reducer.ts: -------------------------------------------------------------------------------- 1 | import { NavActionTypes } from './nav.actionTypes'; 2 | import { NavState } from 'app/store/nav/nav.state'; 3 | import { NavActions } from 'app/store/nav/nav.actions'; 4 | 5 | export function NavReducer( 6 | state: NavState, 7 | action: NavActions.NavAction 8 | ): NavState { 9 | switch (action.type) { 10 | case NavActionTypes.ToggleNavigation: 11 | return { ...state, showNavigation: !state.showNavigation }; 12 | 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/store/nav/nav.state.ts: -------------------------------------------------------------------------------- 1 | export interface NavState { 2 | showNavigation: boolean; 3 | } 4 | 5 | export const DefaultNavState: NavState = { 6 | showNavigation: false 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/store/state.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { EffectsModule } from '@ngrx/effects'; 3 | import { StoreModule } from '@ngrx/store'; 4 | import { defaultAppState } from 'app/store/app.state'; 5 | import { globalReducer } from 'app/store/global/global.reducer'; 6 | import { appReducers } from 'app/store/store.config'; 7 | import { GlobalUserStateModule } from 'app/store/user/globalUserState.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | StoreModule.forRoot(appReducers, { 12 | metaReducers: [globalReducer], 13 | initialState: defaultAppState, 14 | }), 15 | EffectsModule.forRoot([]), 16 | GlobalUserStateModule, 17 | ], 18 | }) 19 | export class StateModule {} 20 | -------------------------------------------------------------------------------- /src/app/store/store.config.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer, combineReducers } from '@ngrx/store'; 2 | import { UserState } from 'app/store/user/user.state'; 3 | 4 | import { NavReducer } from './nav/nav.reducer'; 5 | import { LogInEffects } from './user/logIn/logIn.effects'; 6 | import { logInReducer } from './user/logIn/logIn.reducer'; 7 | import { ActionReducerMap } from '@ngrx/store'; 8 | import { AppState } from 'app/store/app.state'; 9 | 10 | export const appReducers: ActionReducerMap> = { 11 | nav: NavReducer, 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/store/testing/actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { GlobalActions } from 'app/store/global/global.actions'; 2 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 3 | import { NavActions } from 'app/store/nav/nav.actions'; 4 | import { SignUpActions } from 'app/sign-up/state/form/signUpForm.actions'; 5 | import { TodosActions } from 'app/account/todos/state/todos.actions'; 6 | 7 | describe('Actions', () => { 8 | it('actions have unique types', () => { 9 | const hash = {}; 10 | [ 11 | GlobalActions, 12 | LogInActions, 13 | NavActions, 14 | SignUpActions, 15 | TodosActions, 16 | ].forEach(ns => { 17 | for (const key in ns) { 18 | if (ns.hasOwnProperty(key)) { 19 | const type = new ns[key]().type; 20 | if (hash[type]) { 21 | throw new Error(`Duplicate actions of type ${type}`); 22 | } 23 | hash[type] = true; 24 | } 25 | } 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/store/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducerTestHelpers'; 2 | -------------------------------------------------------------------------------- /src/app/store/testing/reducerTestHelpers.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer } from '@ngrx/store'; 2 | 3 | export function shouldNotAlterStateOnUnknownAction(reducer: ActionReducer) { 4 | 5 | it('Should not alter state on unknown action type', () => { 6 | const oldState = 'JustAString'; 7 | expect(reducer(oldState, { type: null })).toBe(oldState); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/store/user/globalUserState.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { StoreModule } from '@ngrx/store'; 3 | import { globalUserReducers } from 'app/store/user/store.config'; 4 | import { 5 | GLOBAL_USER_STORE_KEY, 6 | defaultUserState, 7 | } from 'app/store/user/user.state'; 8 | import { EffectsModule } from '@ngrx/effects'; 9 | import { LogInEffects } from 'app/store/user/logIn/logIn.effects'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | StoreModule.forFeature(GLOBAL_USER_STORE_KEY, globalUserReducers, { 14 | initialState: defaultUserState, 15 | }), 16 | EffectsModule.forFeature([LogInEffects]), 17 | ], 18 | }) 19 | export class GlobalUserStateModule {} 20 | -------------------------------------------------------------------------------- /src/app/store/user/logIn/logIn.actionTypes.ts: -------------------------------------------------------------------------------- 1 | export namespace LogInActionTypes { 2 | export const ShowModal = '[LogIn] Show Log In Modal'; 3 | export const HideModal = '[LogIn] Hide Log In Modal'; 4 | export const LogIn = '[LogIn] Log In'; 5 | export const LogOut = '[LogIn] Log Out'; 6 | export const Failure = '[LogIn] Log In Failure'; 7 | export const Success = '[LogIn] Log In Success'; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/store/user/logIn/logIn.actions.ts: -------------------------------------------------------------------------------- 1 | import { AuthProvider } from '@firebase/auth-types'; 2 | import { Action } from '@ngrx/store'; 3 | import { LogInActionTypes } from 'app/store/user/logIn/logIn.actionTypes'; 4 | 5 | export namespace LogInActions { 6 | export class ShowModal implements Action { 7 | readonly type = LogInActionTypes.ShowModal; 8 | } 9 | 10 | export class HideModal implements Action { 11 | readonly type = LogInActionTypes.HideModal; 12 | } 13 | 14 | export class LogIn implements Action { 15 | readonly type = LogInActionTypes.LogIn; 16 | constructor( 17 | public readonly payload: 18 | | AuthProvider 19 | | { email: string; password: string } 20 | ) {} 21 | } 22 | 23 | export class LogOut implements Action { 24 | readonly type = LogInActionTypes.LogOut; 25 | } 26 | 27 | export class Failure implements Action { 28 | readonly type = LogInActionTypes.Failure; 29 | constructor(public readonly payload: any) {} 30 | } 31 | 32 | export class Success implements Action { 33 | readonly type = LogInActionTypes.Success; 34 | } 35 | 36 | export type LogInAction = ShowModal | HideModal | LogIn | Failure | Success; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/store/user/logIn/logIn.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, NavigationEnd } from '@angular/router'; 3 | import { AuthProvider } from '@firebase/auth-types'; 4 | import { Effect, Actions } from '@ngrx/effects'; 5 | import { AngularFireAuth } from 'angularfire2/auth'; 6 | import { Observable } from 'rxjs/Observable'; 7 | 8 | import { Store } from '@ngrx/store'; 9 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 10 | import { LogInActionTypes } from 'app/store/user/logIn/logIn.actionTypes'; 11 | import { AppState } from 'app/store/app.state'; 12 | 13 | @Injectable() 14 | export class LogInEffects { 15 | @Effect() 16 | logIn$: Observable< 17 | LogInActions.Failure | LogInActions.Success 18 | > = this.actions$ 19 | .ofType(LogInActionTypes.LogIn) 20 | .switchMap(action => { 21 | let request; 22 | 23 | if ((action.payload as any).providerId) { 24 | request = this.auth.auth.signInWithPopup( 25 | action.payload as AuthProvider 26 | ); 27 | } else { 28 | request = this.auth.auth.signInWithEmailAndPassword( 29 | (action.payload as any).email, 30 | (action.payload as any).password 31 | ); 32 | } 33 | return Observable.from(request) 34 | .map(authState => new LogInActions.Success()) 35 | .catch(error => Observable.of(new LogInActions.Failure(error))); 36 | }); 37 | 38 | @Effect({ dispatch: false }) 39 | redirectToProfileOnLoginSuccess$ = this.actions$ 40 | .ofType(LogInActionTypes.Success) 41 | .map(() => this.router.navigateByUrl('/account/profile')); 42 | 43 | // TODO: Move logout into it's own store category + add spinner etc 44 | @Effect({ dispatch: false }) 45 | logOut$ = this.actions$ 46 | .ofType(LogInActionTypes.LogOut) 47 | .switchMap(() => 48 | Observable.from(this.auth.auth.signOut()).map(() => 49 | this.router.navigateByUrl('/') 50 | ) 51 | ); 52 | 53 | @Effect() 54 | hideOnNavigation$ = this.router.events 55 | .filter(e => e instanceof NavigationEnd) 56 | .map(() => new LogInActions.HideModal()); 57 | 58 | constructor( 59 | private actions$: Actions, 60 | private state: Store, 61 | private auth: AngularFireAuth, 62 | private router: Router 63 | ) {} 64 | } 65 | -------------------------------------------------------------------------------- /src/app/store/user/logIn/logIn.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormState, FormStates } from 'app/store/forms/formState'; 2 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 3 | 4 | import { assignDeep } from '../../../helpers'; 5 | import { Messages } from '../../../resources/messages'; 6 | import { shouldNotAlterStateOnUnknownAction } from '../../testing/reducerTestHelpers'; 7 | import { logInReducer } from './logIn.reducer'; 8 | 9 | describe('Log In Reducer', () => { 10 | shouldNotAlterStateOnUnknownAction(logInReducer); 11 | 12 | let oldState: FormState; 13 | 14 | beforeEach(() => { 15 | oldState = assignDeep(FormStates.Default); 16 | }); 17 | 18 | it('Shows the form WHEN ShowModal is called', () => { 19 | oldState.showForm = false; 20 | const newState = logInReducer(oldState, new LogInActions.ShowModal()); 21 | expect(newState.showForm).toBe(true); 22 | }); 23 | 24 | it('Hides the form WHEN HideModal is called', () => { 25 | oldState.showForm = true; 26 | const newState = logInReducer(oldState, new LogInActions.HideModal()); 27 | expect(newState.showForm).toBe(false); 28 | }); 29 | 30 | it('Assigns the requesting state WHEN UpdatePassword is called', () => { 31 | const newState = logInReducer( 32 | oldState, 33 | new LogInActions.LogIn({} as any) 34 | ); 35 | expect(newState).toEqual(FormStates.Requesting); 36 | }); 37 | 38 | it('Displays the correct error message WHEN failure is called', () => { 39 | const newState = logInReducer(oldState, new LogInActions.Failure({})); 40 | expect(newState).toEqual( 41 | FormStates.Failure(Messages.ApiResponse.ServerError) 42 | ); 43 | }); 44 | 45 | it('Displays the correct message WHEN success is called', () => { 46 | const newState = logInReducer(oldState, new LogInActions.Success()); 47 | expect(newState).toEqual(FormStates.Success('')); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/store/user/logIn/logIn.reducer.ts: -------------------------------------------------------------------------------- 1 | import { formReducer } from 'app/store/forms/form.reducer.factory'; 2 | import { LogInActionTypes } from 'app/store/user/logIn/logIn.actionTypes'; 3 | import { FormState } from 'app/store/forms/formState'; 4 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 5 | 6 | export function logInReducer( 7 | state: FormState, 8 | action: LogInActions.LogInAction 9 | ): FormState { 10 | return formReducer({ 11 | show: LogInActionTypes.ShowModal, 12 | hide: LogInActionTypes.HideModal, 13 | request: LogInActionTypes.LogIn, 14 | success: LogInActionTypes.Success, 15 | failure: LogInActionTypes.Failure, 16 | })(state, action); 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/store/user/logIn/login.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Router } from '@angular/router'; 3 | import { provideMockActions } from '@ngrx/effects/testing'; 4 | import { Action } from '@ngrx/store'; 5 | import { AngularFireAuth } from 'angularfire2/auth'; 6 | import { LogInActions } from 'app/store/user/logIn/logIn.actions'; 7 | import { ReplaySubject } from 'rxjs/ReplaySubject'; 8 | 9 | import { assignDeep } from '../../../helpers'; 10 | import { AppState, defaultAppState } from '../../app.state'; 11 | import { Store } from '@ngrx/store'; 12 | import { LogInEffects } from './logIn.effects'; 13 | import { Observable } from 'rxjs/Observable'; 14 | 15 | describe('log in effects', () => { 16 | let state: AppState; 17 | const mockActions$ = new ReplaySubject(1); 18 | let logInEffects: LogInEffects; 19 | 20 | class MockAngularFireAuth { 21 | get auth() { 22 | return { 23 | signInWithEmailAndPassword: 24 | MockAngularFireAuth.prototype.signInWithEmailAndPassword, 25 | signOut: MockAngularFireAuth.prototype.signOut, 26 | }; 27 | } 28 | signInWithEmailAndPassword() { 29 | return Promise.resolve('logged in'); 30 | } 31 | signOut() { 32 | return Promise.resolve('logged out'); 33 | } 34 | } 35 | 36 | class MockRouter { 37 | events = Observable.of(); 38 | navigateByUrl() {} 39 | } 40 | 41 | class MockStore {} 42 | 43 | beforeEach(() => { 44 | state = assignDeep(defaultAppState); 45 | TestBed.configureTestingModule({ 46 | providers: [ 47 | LogInEffects, 48 | provideMockActions(() => mockActions$), 49 | { 50 | provide: Store, 51 | useClass: MockStore, 52 | }, 53 | { provide: AngularFireAuth, useClass: MockAngularFireAuth }, 54 | { provide: Router, useClass: MockRouter }, 55 | ], 56 | }); 57 | 58 | logInEffects = TestBed.get(LogInEffects); 59 | }); 60 | 61 | it('Redirects To Profile On Login Success', done => { 62 | spyOn(MockRouter.prototype, 'navigateByUrl'); 63 | mockActions$.next(new LogInActions.Success()); 64 | logInEffects.redirectToProfileOnLoginSuccess$.subscribe(result => { 65 | expect(MockRouter.prototype.navigateByUrl).toHaveBeenCalledWith( 66 | '/account/profile' 67 | ); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('Redirects to / on logout', done => { 73 | spyOn(MockRouter.prototype, 'navigateByUrl'); 74 | mockActions$.next(new LogInActions.LogOut()); 75 | logInEffects.logOut$.subscribe(result => { 76 | expect(MockRouter.prototype.navigateByUrl).toHaveBeenCalledWith( 77 | '/' 78 | ); 79 | done(); 80 | }); 81 | }); 82 | 83 | it(`Logs in to firebase using the EmailPasswordCredentials signature 84 | WHEN the action payload mtches that signature`, done => { 85 | spyOn( 86 | MockAngularFireAuth.prototype, 87 | 'signInWithEmailAndPassword' 88 | ).and.callThrough(); 89 | 90 | const emailPassword = { 91 | email: 'email@example.com', 92 | password: 'password123', 93 | }; 94 | 95 | mockActions$.next(new LogInActions.LogIn(emailPassword)); 96 | 97 | logInEffects.logIn$.subscribe(result => { 98 | expect( 99 | MockAngularFireAuth.prototype.signInWithEmailAndPassword 100 | ).toHaveBeenCalledTimes(1); 101 | expect( 102 | MockAngularFireAuth.prototype.signInWithEmailAndPassword 103 | ).toHaveBeenCalledWith(emailPassword.email, emailPassword.password); 104 | expect(result).toEqual(new LogInActions.Success()); 105 | done(); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/app/store/user/store.config.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap } from '@ngrx/store/src/models'; 2 | import { UserState } from 'app/store/user/user.state'; 3 | import { logInReducer } from 'app/store/user/logIn/logIn.reducer'; 4 | 5 | export const globalUserReducers: ActionReducerMap = { 6 | logIn: logInReducer, 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/store/user/user.state.ts: -------------------------------------------------------------------------------- 1 | import { FormState, FormStates } from 'app/store/forms/formState'; 2 | 3 | export const GLOBAL_USER_STORE_KEY = 'user'; 4 | 5 | export interface UserState { 6 | logIn: FormState; 7 | } 8 | 9 | export const defaultUserState: UserState = { 10 | logIn: FormStates.Default, 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/store/utils/featureState.ts: -------------------------------------------------------------------------------- 1 | export type AppFeatureState< 2 | TApp extends object, 3 | TKey extends string, 4 | TFeature extends object 5 | > = TApp & { 6 | [key in TKey]: TFeature 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/validators/emailValid.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from '../resources/messages'; 2 | import { FormControl } from '@angular/forms'; 3 | 4 | export const emailRegex = /^.+@.+/; 5 | export const emailValid = (control: FormControl) => emailRegex.test(control.value) ? 6 | null : 7 | { 'EmailInvalid': Messages.Validation.EmailInvalid }; 8 | -------------------------------------------------------------------------------- /src/app/validators/index.ts: -------------------------------------------------------------------------------- 1 | export { emailValid } from './emailValid'; 2 | export { valuesEqual } from './valuesEqual'; 3 | export { minLength } from './minLength'; 4 | export { passwordValid } from './passwordValid'; 5 | -------------------------------------------------------------------------------- /src/app/validators/minLength.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from '@angular/forms'; 2 | 3 | export const minLength = (message: string) => (n: number) => 4 | (control: FormControl) => !control.value || control.value.length < n ? 5 | { 'minLength': message } : 6 | {}; 7 | -------------------------------------------------------------------------------- /src/app/validators/passwordValid.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from '../resources/messages'; 2 | import { FormControl } from '@angular/forms'; 3 | 4 | export const passwordValid = (control: FormControl) => { 5 | if (control.value.length < 6 || !/\d/.test(control.value)) { 6 | return { 'passwordInvalid': Messages.Validation.PasswordInvalid }; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/validators/validUrl.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from '@angular/forms'; 2 | 3 | const urlRegex = /\w+\.\w{2}/; 4 | 5 | export const validUrl = (message: string) => (control: FormControl) => urlRegex.test(control.value); 6 | -------------------------------------------------------------------------------- /src/app/validators/validator.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bave8672/angular-firebase-starter/3cb4873c40d58964ce79d1d9bf400659f1154207/src/app/validators/validator.d.ts -------------------------------------------------------------------------------- /src/app/validators/valuesEqual.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, ValidatorFn, AbstractControl } from '@angular/forms'; 2 | import { TypedValidatorFn } from 'app/shared/forms/typedValidatorFn'; 3 | 4 | export interface UnequalValidationError { 5 | unequal: string; 6 | } 7 | 8 | export const valuesEqual = ( 9 | a: () => AbstractControl, 10 | b: () => AbstractControl, 11 | attachTo?: () => AbstractControl 12 | ) => ( 13 | message: string = 'Values must be the same' 14 | ): TypedValidatorFn => () => { 15 | try { 16 | if (a().value !== b().value) { 17 | const error: UnequalValidationError = { unequal: message }; 18 | (attachTo || b)().setErrors(error); 19 | } 20 | } catch (e) {} 21 | return null; 22 | }; 23 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bave8672/angular-firebase-starter/3cb4873c40d58964ce79d1d9bf400659f1154207/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.d.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseAppConfig } from 'angularfire2'; 2 | 3 | export interface IEnvironmentConfig { 4 | production: boolean; 5 | firebaseConfig: FirebaseAppConfig; 6 | } 7 | -------------------------------------------------------------------------------- /src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseConfig } from '../app/firebase/firebase.config'; 2 | import { IEnvironmentConfig } from './environment.d'; 3 | 4 | export const environment: IEnvironmentConfig = { 5 | production: false, 6 | firebaseConfig: FirebaseConfig 7 | }; 8 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseConfig } from '../app/firebase/firebase.config'; 2 | import { IEnvironmentConfig } from './environment.d'; 3 | 4 | export const environment: IEnvironmentConfig = { 5 | production: true, 6 | firebaseConfig: FirebaseConfig 7 | }; 8 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export { environment } from './environment.dev' 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bave8672/angular-firebase-starter/3cb4873c40d58964ce79d1d9bf400659f1154207/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Firebase App 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | import './vendors'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es6/function'; 25 | import 'core-js/es6/parse-int'; 26 | import 'core-js/es6/parse-float'; 27 | import 'core-js/es6/number'; 28 | import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | import 'core-js/es6/regexp'; 33 | import 'core-js/es6/map'; 34 | import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; 2 | @import '../node_modules/font-awesome/css/font-awesome.min.css'; 3 | @import '../node_modules/bootstrap-social/bootstrap-social.css'; 4 | 5 | .list-group-item { 6 | border: none; 7 | } 8 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | import './vendors'; 15 | import './polyfills'; 16 | 17 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 18 | declare var __karma__: any; 19 | declare var require: any; 20 | 21 | // Prevent Karma from running prematurely. 22 | __karma__.loaded = function () {}; 23 | 24 | // First, initialize the Angular testing environment. 25 | getTestBed().initTestEnvironment( 26 | BrowserDynamicTestingModule, 27 | platformBrowserDynamicTesting() 28 | ); 29 | // Then we find all the tests. 30 | const context = require.context('./', true, /\.spec\.ts$/); 31 | // And load the modules. 32 | context.keys().map(context); 33 | // Finally, start Karma to run the tests. 34 | __karma__.start(); 35 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "es2016", 9 | "dom" 10 | ], 11 | "mapRoot": "./", 12 | "module": "es2015", 13 | "moduleResolution": "node", 14 | "outDir": "../dist/out-tsc", 15 | "sourceMap": true, 16 | "strictNullChecks": false, 17 | "target": "es5", 18 | "typeRoots": [ 19 | "../node_modules/@types" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/vendors.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/Observable'; 2 | import 'rxjs/Subject'; 3 | 4 | import 'rxjs/add/operator/filter'; 5 | import 'rxjs/add/operator/first'; 6 | import 'rxjs/add/operator/map'; 7 | import 'rxjs/add/operator/catch'; 8 | import 'rxjs/add/operator/switchMap'; 9 | 10 | import 'rxjs/add/observable/from'; 11 | import 'rxjs/add/observable/race'; 12 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs", "rxjs/Rx"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "time", 39 | "timeEnd", 40 | "trace" 41 | ], 42 | "no-construct": true, 43 | "no-debugger": true, 44 | "no-duplicate-variable": true, 45 | "no-empty": false, 46 | "no-empty-interface": true, 47 | "no-eval": true, 48 | "no-inferrable-types": [true, "ignore-params"], 49 | "no-shadowed-variable": true, 50 | "no-string-literal": false, 51 | "no-string-throw": true, 52 | "no-trailing-whitespace": true, 53 | "no-unused-expression": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-open-brace", 60 | "check-catch", 61 | "check-else", 62 | "check-whitespace" 63 | ], 64 | "prefer-const": true, 65 | "quotemark": [ 66 | true, 67 | "single" 68 | ], 69 | "radix": true, 70 | "semicolon": [ 71 | "always" 72 | ], 73 | "triple-equals": [ 74 | true, 75 | "allow-null-check" 76 | ], 77 | "typedef-whitespace": [ 78 | true, 79 | { 80 | "call-signature": "nospace", 81 | "index-signature": "nospace", 82 | "parameter": "nospace", 83 | "property-declaration": "nospace", 84 | "variable-declaration": "nospace" 85 | } 86 | ], 87 | "typeof-compare": true, 88 | "unified-signatures": true, 89 | "variable-name": false, 90 | "whitespace": [ 91 | true, 92 | "check-branch", 93 | "check-decl", 94 | "check-operator", 95 | "check-separator", 96 | "check-type" 97 | ], 98 | 99 | "directive-selector": [true, "attribute", "app", "camelCase"], 100 | "component-selector": [true, "element", "app", "kebab-case"], 101 | "use-input-property-decorator": true, 102 | "use-output-property-decorator": true, 103 | "use-host-property-decorator": true, 104 | "no-input-rename": true, 105 | "no-output-rename": true, 106 | "use-life-cycle-interface": true, 107 | "use-pipe-transform-interface": true, 108 | "component-class-suffix": true, 109 | "directive-class-suffix": true, 110 | "no-access-missing-member": true, 111 | "templates-use-public": true, 112 | "invoke-injectable": true 113 | } 114 | } 115 | --------------------------------------------------------------------------------