├── .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 | [](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: ` `,
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 |
3 | {{ todo?.name }}
4 |
5 |
6 |
7 |
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 |
--------------------------------------------------------------------------------
/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 |
4 | Profile Picture
5 |
6 |
7 |
8 | No profile picture
9 |
10 | {{(userState$ | async)?.updatePhotoUrl?.showForm ? 'close' : 'edit'}}
11 |
12 |
13 |
14 |
15 |
16 |
17 | Email Address
18 |
19 |
20 | {{ (user$ | async)?.email }}
21 |
22 | {{(userState$ | async)?.updateEmail?.showForm ? 'close' : 'edit'}}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Password
31 |
32 |
33 |
34 | {{(userState$ | async)?.updatePassword?.showForm ? 'close' : 'edit'}}
35 |
36 |
37 |
38 |
39 |
40 |
Email Verified
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
13 |
14 |
15 | Profile
16 | Account
17 | Log Out
18 |
19 |
20 | Sign up
21 | Log In
22 |
23 |
24 |
25 |
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 |
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 |
2 |
3 |
4 |
5 |
6 | {{ requestingText }}
7 |
8 |
9 |
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 |
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 |
--------------------------------------------------------------------------------