├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── browserslist ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── proxy.json ├── server ├── auth.route.ts ├── db-data.ts ├── get-courses.route.ts ├── save-course.route.ts ├── search-lessons.route.ts ├── server.ts └── server.tsconfig.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.actions.ts │ │ ├── auth.effects.spec.ts │ │ ├── auth.effects.ts │ │ ├── auth.guard.ts │ │ ├── auth.module.ts │ │ ├── auth.reducer.spec.ts │ │ ├── auth.reducer.ts │ │ ├── auth.selectors.ts │ │ ├── auth.service.ts │ │ └── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ └── login.component.ts │ ├── courses │ │ ├── course-dialog │ │ │ ├── course-dialog.component.css │ │ │ ├── course-dialog.component.html │ │ │ └── course-dialog.component.ts │ │ ├── course.actions.ts │ │ ├── course.effects.ts │ │ ├── course.reducers.ts │ │ ├── course.selectors.ts │ │ ├── course │ │ │ ├── course.component.css │ │ │ ├── course.component.html │ │ │ └── course.component.ts │ │ ├── courses-card-list │ │ │ ├── courses-card-list.component.css │ │ │ ├── courses-card-list.component.html │ │ │ └── courses-card-list.component.ts │ │ ├── courses.module.ts │ │ ├── home │ │ │ ├── home.component.css │ │ │ ├── home.component.html │ │ │ └── home.component.ts │ │ ├── lessons.reducers.ts │ │ ├── model │ │ │ ├── course.ts │ │ │ └── lesson.ts │ │ └── services │ │ │ ├── course.resolver.ts │ │ │ ├── courses.service.ts │ │ │ └── lessons.datasource.ts │ ├── model │ │ └── user.model.ts │ ├── reducers │ │ └── index.ts │ └── shared │ │ └── utils.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /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 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Angular University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This course is now archived, and has been replaced by [NgRx (with Ngrx Data) - The Complete Guide](https://github.com/angular-university/ngrx-course) 3 | 4 | 5 | ## Angular Ngrx Course 6 | 7 | This repository contains the code of the [Angular Ngrx Course](https://angular-university.io/course/angular-ngrx-course). 8 | 9 | This course repository is updated to Angular v8, and there is a package-lock.json file available, for avoiding semantic versioning installation issues. 10 | 11 | ![Angular Ngrx Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-ngrx-course.png) 12 | 13 | 14 | # Installation pre-requisites 15 | 16 | IMPORTANT: Please use NPM 5 or above, to make sure the package-lock.json is used. 17 | 18 | For running this project we need and npm installed on our machine. These are some tutorials to install node in different operating systems: 19 | 20 | *Its important to install the latest version of Node* 21 | 22 | - [Install Node and NPM on Windows](https://www.youtube.com/watch?v=8ODS6RM6x7g) 23 | - [Install Node and NPM on Linux](https://www.youtube.com/watch?v=yUdHk-Dk_BY) 24 | - [Install Node and NPM on Mac](https://www.youtube.com/watch?v=Imj8PgG3bZU) 25 | 26 | 27 | # Installing the Angular CLI 28 | 29 | With the following command the angular-cli will be installed globally in your machine: 30 | 31 | npm install -g @angular/cli 32 | 33 | 34 | # How To install this repository 35 | 36 | We can install the master branch using the following commands: 37 | 38 | git clone https://github.com/angular-university/angular-ngrx-course.git 39 | 40 | This repository is made of several separate npm modules, that are installable separately. For example, to run the au-input module, we can do the following: 41 | 42 | cd angular-ngrx-course 43 | npm install 44 | 45 | Its also possible to install the modules as usual using npm: 46 | 47 | npm install 48 | 49 | NPM 5 or above has the big advantage that if you use it you will be installing the exact same dependencies than I installed in my machine, so you wont run into issues caused by semantic versioning updates. 50 | 51 | This should take a couple of minutes. If there are issues, please post the complete error message in the Questions section of the course. 52 | 53 | # To Run the Development Backend Server 54 | 55 | We can start the sample application backend with the following command: 56 | 57 | npm run server 58 | 59 | This is a small Node REST API server. 60 | 61 | # To run the Development UI Server 62 | 63 | To run the frontend part of our code, we will use the Angular CLI: 64 | 65 | npm start 66 | 67 | The application is visible at port 4200: [http://localhost:4200](http://localhost:4200) 68 | 69 | 70 | 71 | # Important 72 | 73 | This repository has multiple branches, have a look at the beginning of each section to see the name of the branch. 74 | 75 | At certain points along the course, you will be asked to checkout other remote branches other than master. You can view all branches that you have available remotely using the following command: 76 | 77 | git branch -a 78 | 79 | The remote branches have their starting in origin, such as for example 1-navigation-and-containers. 80 | 81 | We can checkout the remote branch and start tracking it with a local branch that has the same name, by using the following command: 82 | 83 | git checkout -b 1-auth origin/1-auth 84 | 85 | It's also possible to download a ZIP file for a given branch, using the branch dropdown on this page on the top left, and then selecting the Clone or Download / Download as ZIP button. 86 | 87 | # Other Courses 88 | 89 | # Angular PWA Course 90 | 91 | If you are looking for the [Angular PWA Course](https://angular-university.io/course/angular-pwa-course), the repo with the full code can be found here: 92 | 93 | ![Angular PWA Course - Build the future of the Web Today](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png) 94 | 95 | # Angular Security Masterclass 96 | 97 | If you are looking for the [Angular Security Masterclass](https://angular-university.io/course/angular-security-course), the repo with the full code can be found here: 98 | 99 | [Angular Security Masterclass](https://github.com/angular-university/angular-security-course). 100 | 101 | ![Angular Security Masterclass](https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png) 102 | 103 | # Angular Advanced Library Laboratory Course 104 | 105 | If you are looking for the Angular Advanced Course, the repo with the full code can be found here: 106 | 107 | [Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-university.io/course/angular-advanced-course). 108 | 109 | ![Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png) 110 | 111 | 112 | ## RxJs and Reactive Patterns Angular Architecture Course 113 | 114 | If you are looking for the RxJs and Reactive Patterns Angular Architecture Course code, the repo with the full code can be found here: 115 | 116 | [RxJs and Reactive Patterns Angular Architecture Course](https://angular-university.io/course/reactive-angular-architecture-course) 117 | 118 | ![RxJs and Reactive Patterns Angular Architecture Course](https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png) 119 | 120 | 121 | 122 | ## Angular Ngrx Reactive Extensions Architecture Course 123 | 124 | If you are looking for the Angular Ngrx Reactive Extensions Architecture Course code, the repo with the full code can be found here: 125 | 126 | [Angular Ngrx Reactive Extensions Architecture Course](https://angular-university.io/course/angular2-ngrx) 127 | 128 | [Github repo for this course](https://github.com/angular-university/ngrx-course) 129 | 130 | ![Angular Ngrx Course](https://angular-academy.s3.amazonaws.com/thumbnails/ngrx-angular.png) 131 | 132 | 133 | 134 | ## Angular 2 and Firebase - Build a Web Application Course 135 | 136 | If you are looking for the Angular 2 and Firebase - Build a Web Application Course code, the repo with the full code can be found here: 137 | 138 | [Angular 2 and Firebase - Build a Web Application](https://angular-university.io/course/build-an-application-with-angular2) 139 | 140 | [Github repo for this course](https://github.com/angular-university/angular-firebase-app) 141 | 142 | ![Angular firebase course](https://angular-academy.s3.amazonaws.com/thumbnails/angular_app-firebase-small.jpg) 143 | 144 | 145 | ## Complete Typescript 2 Course - Build A REST API 146 | 147 | If you are looking for the Complete Typescript 2 Course - Build a REST API, the repo with the full code can be found here: 148 | 149 | [https://angular-university.io/course/typescript-2-tutorial](https://github.com/angular-university/complete-typescript-course) 150 | 151 | [Github repo for this course](https://github.com/angular-university/complete-typescript-course) 152 | 153 | ![Complete Typescript Course](https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png) 154 | 155 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-ngrx-course": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico" 22 | ], 23 | "styles": [ 24 | "src/styles.scss" 25 | ], 26 | "scripts": [] 27 | }, 28 | "configurations": { 29 | "production": { 30 | "optimization": true, 31 | "outputHashing": "all", 32 | "sourceMap": false, 33 | "extractCss": true, 34 | "namedChunks": false, 35 | "aot": true, 36 | "extractLicenses": true, 37 | "vendorChunk": false, 38 | "buildOptimizer": true, 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ] 45 | } 46 | } 47 | }, 48 | "serve": { 49 | "builder": "@angular-devkit/build-angular:dev-server", 50 | "options": { 51 | "browserTarget": "angular-ngrx-course:build" 52 | }, 53 | "configurations": { 54 | "production": { 55 | "browserTarget": "angular-ngrx-course:build:production" 56 | } 57 | } 58 | }, 59 | "extract-i18n": { 60 | "builder": "@angular-devkit/build-angular:extract-i18n", 61 | "options": { 62 | "browserTarget": "angular-ngrx-course:build" 63 | } 64 | }, 65 | "test": { 66 | "builder": "@angular-devkit/build-angular:karma", 67 | "options": { 68 | "main": "src/test.ts", 69 | "karmaConfig": "./karma.conf.js", 70 | "polyfills": "src/polyfills.ts", 71 | "tsConfig": "src/tsconfig.spec.json", 72 | "scripts": [], 73 | "styles": [ 74 | "src/styles.scss" 75 | ], 76 | "assets": [ 77 | "src/assets", 78 | "src/favicon.ico" 79 | ] 80 | } 81 | }, 82 | "lint": { 83 | "builder": "@angular-devkit/build-angular:tslint", 84 | "options": { 85 | "tsConfig": [ 86 | "src/tsconfig.app.json", 87 | "src/tsconfig.spec.json" 88 | ], 89 | "exclude": [ 90 | "**/node_modules/**" 91 | ] 92 | } 93 | } 94 | } 95 | }, 96 | "angular-ngrx-course-e2e": { 97 | "root": "", 98 | "sourceRoot": "", 99 | "projectType": "application", 100 | "architect": { 101 | "e2e": { 102 | "builder": "@angular-devkit/build-angular:protractor", 103 | "options": { 104 | "protractorConfig": "./protractor.conf.js", 105 | "devServerTarget": "angular-ngrx-course:serve" 106 | } 107 | }, 108 | "lint": { 109 | "builder": "@angular-devkit/build-angular:tslint", 110 | "options": { 111 | "tsConfig": [ 112 | "e2e/tsconfig.e2e.json" 113 | ], 114 | "exclude": [ 115 | "**/node_modules/**" 116 | ] 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "defaultProject": "angular-ngrx-course", 123 | "schematics": { 124 | "@ngrx/schematics:component": { 125 | "prefix": "", 126 | "styleext": "scss" 127 | }, 128 | "@ngrx/schematics:directive": { 129 | "prefix": "" 130 | } 131 | }, 132 | "cli": { 133 | "defaultCollection": "@ngrx/schematics" 134 | } 135 | } -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('angular-ngrx-course App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ngrx-course", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --proxy-config ./proxy.json", 8 | "server": "ts-node -P ./server/server.tsconfig.json ./server/server.ts", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular-devkit/schematics": "^8.0.0", 17 | "@angular/animations": "^8.0.0", 18 | "@angular/cdk": "^8.0.0", 19 | "@angular/common": "^8.0.0", 20 | "@angular/compiler": "^8.0.0", 21 | "@angular/core": "^8.0.0", 22 | "@angular/forms": "^8.0.0", 23 | "@angular/material": "^8.0.0", 24 | "@angular/material-moment-adapter": "^8.0.0", 25 | "@angular/platform-browser": "^8.0.0", 26 | "@angular/platform-browser-dynamic": "^8.0.0", 27 | "@angular/router": "^8.0.0", 28 | "@ngrx/effects": "^7.0.0", 29 | "@ngrx/entity": "^7.0.0", 30 | "@ngrx/router-store": "^7.0.0", 31 | "@ngrx/store": "^7.0.0", 32 | "@ngrx/store-devtools": "^7.0.0", 33 | "body-parser": "^1.18.2", 34 | "core-js": "^2.4.1", 35 | "express": "^4.16.2", 36 | "hammerjs": "^2.0.8", 37 | "moment": "^2.22.2", 38 | "ngrx-store-freeze": "^0.2.1", 39 | "rxjs": "^6.3.3", 40 | "zone.js": "~0.9.1" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/build-angular": "~0.800.0", 44 | "@angular/cli": "^8.0.1", 45 | "@angular/compiler-cli": "^8.0.0", 46 | "@angular/language-service": "^8.0.0", 47 | "@ngrx/schematics": "^7.0.0", 48 | "@types/express": "^4.0.39", 49 | "@types/jasmine": "~2.5.53", 50 | "@types/jasminewd2": "~2.0.2", 51 | "@types/node": "~6.0.60", 52 | "codelyzer": "^5.0.1", 53 | "jasmine-core": "~2.6.2", 54 | "jasmine-spec-reporter": "~4.1.0", 55 | "karma": "^4.1.0", 56 | "karma-chrome-launcher": "~2.1.1", 57 | "karma-cli": "~1.0.1", 58 | "karma-coverage-istanbul-reporter": "^1.2.1", 59 | "karma-jasmine": "~1.1.0", 60 | "karma-jasmine-html-reporter": "^0.2.2", 61 | "protractor": "^6.0.0", 62 | "ts-node": "~3.2.0", 63 | "tslint": "~5.7.0", 64 | "typescript": "~3.4.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:9000", 4 | "secure": false 5 | } 6 | } -------------------------------------------------------------------------------- /server/auth.route.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Request, Response} from 'express'; 4 | import {authenticate} from "./db-data"; 5 | 6 | 7 | 8 | 9 | export function loginUser(req: Request, res: Response) { 10 | 11 | console.log("User login attempt ..."); 12 | 13 | const {email, password} = req.body; 14 | 15 | const user = authenticate(email, password); 16 | 17 | if (user) { 18 | res.status(200).json({id:user.id, email: user.email}); 19 | } 20 | else { 21 | res.sendStatus(403); 22 | } 23 | 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /server/db-data.ts: -------------------------------------------------------------------------------- 1 | 2 | export const USERS = { 3 | 1: { 4 | id: 1, 5 | email: 'test@angular-university.io', 6 | password:'test' 7 | } 8 | 9 | }; 10 | 11 | 12 | export const COURSES = { 13 | 0: { 14 | id: 0, 15 | description: "Angular Ngrx Course", 16 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-ngrx-course.png', 17 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 18 | longDescription: "Learn the modern Ngrx Ecosystem, including Store, Effects, Router Store, Ngrx Entity, Dev Tools and Schematics.", 19 | category: 'BEGINNER', 20 | lessonsCount: 6, 21 | promo:true 22 | }, 23 | 1: { 24 | id: 1, 25 | description: "Angular for Beginners", 26 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular2-for-beginners-small-v2.png', 27 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 28 | longDescription: "Establish a solid layer of fundamentals, learn what's under the hood of Angular", 29 | category: 'BEGINNER', 30 | lessonsCount: 10, 31 | promo:true 32 | }, 33 | 2: { 34 | id: 2, 35 | description: 'Angular Security Course - Web Security Fundamentals', 36 | longDescription: "Learn Web Security Fundamentals and apply them to defend an Angular / Node Application from multiple types of attacks.", 37 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png', 38 | courseListIcon: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/lock-v2.png', 39 | category: 'ADVANCED', 40 | lessonsCount: 11, 41 | promo:false 42 | }, 43 | 3: { 44 | id: 3, 45 | description: 'Angular PWA - Progressive Web Apps Course', 46 | longDescription: "

Learn Angular Progressive Web Applications, build the future of the Web Today.", 47 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png', 48 | courseListIcon: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/alien.png', 49 | category: 'ADVANCED', 50 | lessonsCount: 8, 51 | promo:false 52 | }, 53 | 4: { 54 | id: 4, 55 | description: 'Angular NgRx Store Reactive Extensions Architecture Course', 56 | longDescription: "Learn how to the Angular NgRx Reactive Extensions and its Tooling to build a complete application.", 57 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/ngrx-angular.png', 58 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/thumbnails/ngrx-small.png', 59 | category: 'ADVANCED', 60 | promo:false 61 | }, 62 | 5: { 63 | id: 5, 64 | description: 'Angular Advanced Library Laboratory: Build Your Own Library', 65 | longDescription: "Learn Advanced Angular functionality typically used in Library Development. Advanced Components, Directives, Testing, Npm", 66 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png', 67 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular-advanced-lesson-icon.png', 68 | category: 'ADVANCED', 69 | promo:false 70 | }, 71 | 6: { 72 | id: 6, 73 | description: 'The Complete Typescript Course', 74 | longDescription: "Complete Guide to Typescript From Scratch: Learn the language in-depth and use it to build a Node REST API.", 75 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png', 76 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-lesson.png', 77 | category: 'BEGINNER', 78 | promo:false 79 | }, 80 | 7: { 81 | id: 7, 82 | description: 'Rxjs and Reactive Patterns Angular Architecture Course', 83 | longDescription: "Learn the core RxJs Observable Pattern as well and many other Design Patterns for building Reactive Angular Applications.", 84 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png', 85 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/course-logos/observables_rxjs.png', 86 | category: 'BEGINNER', 87 | promo:false 88 | }, 89 | 8: { 90 | id:8, 91 | description: "Angular Material Course", 92 | iconUrl: "https://s3-us-west-1.amazonaws.com/angular-university/course-images/material_design.png", 93 | longDescription: "Build Applications with the official Angular Widget Library", 94 | category: 'ADVANCED', 95 | promo:false 96 | }, 97 | }; 98 | 99 | 100 | export const LESSONS = { 101 | 102 | 1: { 103 | id: 1, 104 | "description": "Angular Tutorial For Beginners - Build Your First App - Hello World Step By Step", 105 | "duration": "4:17", 106 | "seqNo": 1, 107 | courseId: 1 108 | }, 109 | 2: { 110 | id: 2, 111 | "description": "Building Your First Component - Component Composition", 112 | "duration": "2:07", 113 | "seqNo": 2, 114 | courseId: 1 115 | }, 116 | 3: { 117 | id: 3, 118 | "description": "Component @Input - How To Pass Input Data To an Component", 119 | "duration": "2:33", 120 | "seqNo": 3, 121 | courseId: 1 122 | }, 123 | 4: { 124 | id: 4, 125 | "description": " Component Events - Using @Output to create custom events", 126 | "duration": "4:44", 127 | "seqNo": 4, 128 | courseId: 1 129 | }, 130 | 5: { 131 | id: 5, 132 | "description": " Component Templates - Inline Vs External", 133 | "duration": "2:55", 134 | "seqNo": 5, 135 | courseId: 1 136 | }, 137 | 6: { 138 | id: 6, 139 | "description": "Styling Components - Learn About Component Style Isolation", 140 | "duration": "3:27", 141 | "seqNo": 6, 142 | courseId: 1 143 | }, 144 | 7: { 145 | id: 7, 146 | "description": " Component Interaction - Extended Components Example", 147 | "duration": "9:22", 148 | "seqNo": 7, 149 | courseId: 1 150 | }, 151 | 8: { 152 | id: 8, 153 | "description": " Components Tutorial For Beginners - Components Exercise !", 154 | "duration": "1:26", 155 | "seqNo": 8, 156 | courseId: 1 157 | }, 158 | 9: { 159 | id: 9, 160 | "description": " Components Tutorial For Beginners - Components Exercise Solution Inside", 161 | "duration": "2:08", 162 | "seqNo": 9, 163 | courseId: 1 164 | }, 165 | 10: { 166 | id: 10, 167 | "description": " Directives - Inputs, Output Event Emitters and How To Export Template References", 168 | "duration": "4:01", 169 | "seqNo": 10, 170 | courseId: 1 171 | }, 172 | 173 | 174 | // Security Course 175 | 11: { 176 | id: 11, 177 | "description": "Course Helicopter View", 178 | "duration": "08:19", 179 | "seqNo": 1, 180 | courseId: 2 181 | }, 182 | 183 | 12: { 184 | id: 12, 185 | "description": "Installing Git, Node, NPM and Choosing an IDE", 186 | "duration": "04:17", 187 | "seqNo": 2, 188 | courseId: 2 189 | }, 190 | 191 | 13: { 192 | id: 13, 193 | "description": "Installing The Lessons Code - Learn Why Its Essential To Use NPM 5", 194 | "duration": "06:05", 195 | "seqNo": 3, 196 | courseId: 2 197 | }, 198 | 199 | 14: { 200 | id: 14, 201 | "description": "How To Run Node In TypeScript With Hot Reloading", 202 | "duration": "03:57", 203 | "seqNo": 4, 204 | courseId: 2 205 | }, 206 | 207 | 15: { 208 | id: 15, 209 | "description": "Guided Tour Of The Sample Application", 210 | "duration": "06:00", 211 | "seqNo": 5, 212 | courseId: 2 213 | }, 214 | 16: { 215 | id: 16, 216 | "description": "Client Side Authentication Service - API Design", 217 | "duration": "04:53", 218 | "seqNo": 6, 219 | courseId: 2 220 | }, 221 | 17: { 222 | id: 17, 223 | "description": "Client Authentication Service - Design and Implementation", 224 | "duration": "09:14", 225 | "seqNo": 7, 226 | courseId: 2 227 | }, 228 | 18: { 229 | id: 18, 230 | "description": "The New Angular HTTP Client - Doing a POST Call To The Server", 231 | "duration": "06:08", 232 | "seqNo": 8, 233 | courseId: 2 234 | }, 235 | 19: { 236 | id: 19, 237 | "description": "User Sign Up Server-Side Implementation in Express", 238 | "duration": "08:50", 239 | "seqNo": 9, 240 | courseId: 2 241 | }, 242 | 20: { 243 | id: 20, 244 | "description": "Introduction To Cryptographic Hashes - A Running Demo", 245 | "duration": "05:46", 246 | "seqNo": 10, 247 | courseId: 2 248 | }, 249 | 21: { 250 | id: 21, 251 | "description": "Some Interesting Properties Of Hashing Functions - Validating Passwords", 252 | "duration": "06:31", 253 | "seqNo": 11, 254 | courseId: 2 255 | }, 256 | 257 | 258 | // PWA course 259 | 260 | 22: { 261 | id: 22, 262 | "description": "Course Kick-Off - Install Node, NPM, IDE And Service Workers Section Code", 263 | "duration": "07:19", 264 | "seqNo": 1, 265 | courseId: 3 266 | }, 267 | 23: { 268 | id: 23, 269 | "description": "Service Workers In a Nutshell - Service Worker Registration", 270 | "duration": "6:59", 271 | "seqNo": 2, 272 | courseId: 3 273 | }, 274 | 24: { 275 | id: 24, 276 | "description": "Service Workers Hello World - Lifecycle Part 1 and PWA Chrome Dev Tools", 277 | "duration": "7:28", 278 | "seqNo": 3, 279 | courseId: 3 280 | }, 281 | 25: { 282 | id: 25, 283 | "description": "Service Workers and Application Versioning - Install & Activate Lifecycle Phases", 284 | "duration": "10:17", 285 | "seqNo": 4, 286 | courseId: 3 287 | }, 288 | 289 | 26: { 290 | id: 26, 291 | "description": "Downloading The Offline Page - The Service Worker Installation Phase", 292 | "duration": "09:50", 293 | "seqNo": 5, 294 | courseId: 3 295 | }, 296 | 27: { 297 | id: 27, 298 | "description": "Introduction to the Cache Storage PWA API", 299 | "duration": "04:44", 300 | "seqNo": 6, 301 | courseId: 3 302 | }, 303 | 28: { 304 | id: 28, 305 | "description": "View Service Workers HTTP Interception Features In Action", 306 | "duration": "06:07", 307 | "seqNo": 7, 308 | courseId: 3 309 | }, 310 | 29: { 311 | id: 29, 312 | "description": "Service Workers Error Handling - Serving The Offline Page", 313 | "duration": "5:38", 314 | "seqNo": 8, 315 | courseId: 3 316 | }, 317 | 30: { 318 | id: 30, 319 | "description": "Welcome to the Angular Ngrx Course", 320 | "duration": "6:53", 321 | "seqNo": 1, 322 | courseId: 0 323 | 324 | }, 325 | 31: { 326 | id: 31, 327 | "description": "The Angular Ngrx Architecture Course - Helicopter View", 328 | "duration": "5:52", 329 | "seqNo": 2, 330 | courseId: 0 331 | }, 332 | 32: { 333 | id: 32, 334 | "description": "The Origins of Flux - Understanding the Famous Facebook Bug Problem", 335 | "duration": "8:17", 336 | "seqNo": 3, 337 | courseId: 0 338 | }, 339 | 33: { 340 | id: 33, 341 | "description": "Custom Global Events - Why Don't They Scale In Complexity?", 342 | "duration": "7:47", 343 | "seqNo": 4, 344 | courseId: 0 345 | }, 346 | 34: { 347 | id: 34, 348 | "description": "The Flux Architecture - How Does it Solve Facebook Counter Problem?", 349 | "duration": "9:22", 350 | "seqNo": 5, 351 | courseId: 0 352 | }, 353 | 35: { 354 | id: 35, 355 | "description": "Unidirectional Data Flow And The Angular Development Mode", 356 | "duration": "7:07", 357 | "seqNo": 6, 358 | courseId: 0 359 | } 360 | 361 | }; 362 | 363 | export function findCourseById(courseId:number) { 364 | return COURSES[courseId]; 365 | } 366 | 367 | export function findLessonsForCourse(courseId:number) { 368 | return Object.values(LESSONS).filter(lesson => lesson.courseId == courseId); 369 | } 370 | 371 | 372 | export function authenticate(email:string, password:string) { 373 | 374 | const user:any = Object.values(USERS).find(user => user.email === email); 375 | 376 | if (user && user.password == password) { 377 | return user; 378 | } 379 | else { 380 | return undefined; 381 | } 382 | 383 | } 384 | -------------------------------------------------------------------------------- /server/get-courses.route.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Request, Response} from 'express'; 4 | import {COURSES} from "./db-data"; 5 | 6 | 7 | 8 | export function getAllCourses(req: Request, res: Response) { 9 | 10 | console.log("Retrieving courses data ..."); 11 | 12 | res.status(200).json({payload:Object.values(COURSES)}); 13 | 14 | } 15 | 16 | 17 | export function getCourseById(req: Request, res: Response) { 18 | 19 | const courseId = req.params["id"]; 20 | 21 | const courses = Object.values(COURSES); 22 | 23 | const course = courses.find(course => course.id == courseId); 24 | 25 | res.status(200).json(course); 26 | } -------------------------------------------------------------------------------- /server/save-course.route.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from 'express'; 2 | import {COURSES} from "./db-data"; 3 | 4 | 5 | export function saveCourse(req: Request, res: Response) { 6 | 7 | console.log("Saving course ..."); 8 | 9 | const id = req.params["id"], 10 | changes = req.body; 11 | 12 | COURSES[id] = { 13 | ...COURSES[id], 14 | ...changes 15 | }; 16 | 17 | res.status(200).json(COURSES[id]); 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /server/search-lessons.route.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import {Request, Response} from 'express'; 5 | import {LESSONS} from "./db-data"; 6 | import {setTimeout} from "timers"; 7 | 8 | 9 | 10 | export function searchLessons(req: Request, res: Response) { 11 | 12 | console.log('Searching for lessons ...'); 13 | 14 | // const error = (Math.random() >= 0.5); 15 | 16 | // if (error) { 17 | // console.log("ERROR loading lessons!"); 18 | // res.status(500).json({message: 'random error occurred.'}); 19 | // } 20 | // else { 21 | 22 | 23 | const queryParams = req.query; 24 | 25 | const courseId = queryParams.courseId, 26 | filter = queryParams.filter || '', 27 | sortOrder = queryParams.sortOrder, 28 | pageNumber = parseInt(queryParams.pageNumber) || 0, 29 | pageSize = parseInt(queryParams.pageSize); 30 | 31 | let lessons = Object.values(LESSONS).filter(lesson => lesson.courseId == courseId).sort((l1, l2) => l1.id - l2.id); 32 | 33 | if (filter) { 34 | lessons = lessons.filter(lesson => lesson.description.trim().toLowerCase().search(filter.toLowerCase()) >= 0); 35 | } 36 | 37 | if (sortOrder == "desc") { 38 | lessons = lessons.reverse(); 39 | } 40 | 41 | const initialPos = pageNumber * pageSize; 42 | 43 | const lessonsPage = lessons.slice(initialPos, initialPos + pageSize); 44 | 45 | setTimeout(() => { 46 | res.status(200).json({payload: lessonsPage}); 47 | },1000); 48 | 49 | // } 50 | 51 | 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as express from 'express'; 4 | import {Application} from "express"; 5 | import {getAllCourses, getCourseById} from "./get-courses.route"; 6 | import {searchLessons} from "./search-lessons.route"; 7 | import {loginUser} from "./auth.route"; 8 | import {saveCourse} from "./save-course.route"; 9 | 10 | const bodyParser = require('body-parser'); 11 | 12 | 13 | 14 | const app: Application = express(); 15 | 16 | 17 | app.use(bodyParser.json()); 18 | 19 | 20 | app.route('/api/login').post(loginUser); 21 | 22 | app.route('/api/courses').get(getAllCourses); 23 | 24 | app.route('/api/courses/:id').put(saveCourse); 25 | 26 | app.route('/api/courses/:id').get(getCourseById); 27 | 28 | app.route('/api/lessons').get(searchLessons); 29 | 30 | 31 | 32 | 33 | const httpServer = app.listen(9000, () => { 34 | console.log("HTTP REST API Server running at http://localhost:" + httpServer.address().port); 35 | }); 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /server/server.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | 2 | >>> body { 3 | margin: 0; 4 | } 5 | 6 | main { 7 | margin: 30px; 8 | } 9 | 10 | .menu-button { 11 | background: #607d8b; 12 | color: white; 13 | border: none; 14 | cursor:pointer; 15 | outline:none; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | library_books 8 | Courses 9 | 10 | 11 | 12 | account_circle 13 | Login 14 | 15 | 16 | 17 | exit_to_app 18 | Logout 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [ 8 | RouterTestingModule 9 | ], 10 | declarations: [ 11 | AppComponent 12 | ], 13 | }).compileComponents(); 14 | })); 15 | it('should create the app', async(() => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.debugElement.componentInstance; 18 | expect(app).toBeTruthy(); 19 | })); 20 | it(`should have as title 'app'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app'); 24 | })); 25 | it('should render title in a h1 tag', async(() => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 30 | })); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {select, Store} from "@ngrx/store"; 3 | import {Observable} from "rxjs"; 4 | import {AppState} from './reducers'; 5 | import {Logout} from './auth/auth.actions'; 6 | import {map} from 'rxjs/operators'; 7 | import {isLoggedIn, isLoggedOut} from './auth/auth.selectors'; 8 | import {Router} from '@angular/router'; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: './app.component.html', 13 | styleUrls: ['./app.component.css'] 14 | }) 15 | export class AppComponent implements OnInit { 16 | 17 | isLoggedIn$: Observable; 18 | 19 | isLoggedOut$: Observable; 20 | 21 | 22 | constructor(private store: Store, private router: Router) { 23 | 24 | } 25 | 26 | ngOnInit() { 27 | 28 | this.isLoggedIn$ = this.store 29 | .pipe( 30 | select(isLoggedIn) 31 | ); 32 | 33 | this.isLoggedOut$ = this.store 34 | .pipe( 35 | select(isLoggedOut) 36 | ); 37 | 38 | } 39 | 40 | logout() { 41 | 42 | this.store.dispatch(new Logout()); 43 | 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; 6 | import {MatMenuModule} from '@angular/material/menu'; 7 | import {MatIconModule} from '@angular/material/icon'; 8 | 9 | import { MatListModule } from "@angular/material/list"; 10 | import { MatSidenavModule } from "@angular/material/sidenav"; 11 | import { MatToolbarModule } from "@angular/material/toolbar"; 12 | import {HttpClientModule} from "@angular/common/http"; 13 | 14 | import {RouterModule, Routes} from "@angular/router"; 15 | import {AuthModule} from "./auth/auth.module"; 16 | import { StoreModule } from '@ngrx/store'; 17 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 18 | import { environment } from '../environments/environment'; 19 | import {RouterStateSerializer, StoreRouterConnectingModule} from "@ngrx/router-store"; 20 | 21 | import { EffectsModule } from '@ngrx/effects'; 22 | import { reducers, metaReducers } from './reducers'; 23 | import {AuthGuard} from './auth/auth.guard'; 24 | import {CustomSerializer} from './shared/utils'; 25 | 26 | 27 | const routes: Routes = [ 28 | { 29 | path: 'courses', 30 | loadChildren: () => import('./courses/courses.module').then(m => m.CoursesModule), 31 | canActivate: [AuthGuard], 32 | }, 33 | { 34 | path: "**", 35 | redirectTo: '/' 36 | } 37 | ]; 38 | 39 | 40 | @NgModule({ 41 | declarations: [ 42 | AppComponent 43 | ], 44 | imports: [ 45 | BrowserModule, 46 | BrowserAnimationsModule, 47 | RouterModule.forRoot(routes), 48 | HttpClientModule, 49 | MatMenuModule, 50 | MatIconModule, 51 | MatSidenavModule, 52 | MatListModule, 53 | MatToolbarModule, 54 | AuthModule.forRoot(), 55 | StoreModule.forRoot(reducers, { metaReducers }), 56 | !environment.production ? StoreDevtoolsModule.instrument() : [], 57 | EffectsModule.forRoot([]), 58 | StoreRouterConnectingModule.forRoot({stateKey:'router'}) 59 | ], 60 | providers: [ 61 | { provide: RouterStateSerializer, useClass: CustomSerializer } 62 | ], 63 | bootstrap: [AppComponent] 64 | }) 65 | export class AppModule { 66 | } 67 | -------------------------------------------------------------------------------- /src/app/auth/auth.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import {User} from '../model/user.model'; 3 | 4 | 5 | 6 | export enum AuthActionTypes { 7 | LoginAction = '[Login] Action', 8 | LogoutAction = '[Logout] Action', 9 | } 10 | 11 | 12 | export class Login implements Action { 13 | 14 | readonly type = AuthActionTypes.LoginAction; 15 | 16 | constructor(public payload: {user: User}) { 17 | 18 | } 19 | } 20 | 21 | 22 | export class Logout implements Action { 23 | 24 | readonly type = AuthActionTypes.LogoutAction; 25 | 26 | 27 | } 28 | 29 | 30 | export type AuthActions = Login | Logout; 31 | -------------------------------------------------------------------------------- /src/app/auth/auth.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { provideMockActions } from '@ngrx/effects/testing'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { AuthEffects } from './auth.effects'; 6 | 7 | describe('AuthService', () => { 8 | let actions$: Observable; 9 | let effects: AuthEffects; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | providers: [ 14 | AuthEffects, 15 | provideMockActions(() => actions$) 16 | ] 17 | }); 18 | 19 | effects = TestBed.get(AuthEffects); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(effects).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/auth/auth.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {Actions, Effect, ofType} from '@ngrx/effects'; 3 | import {AuthActionTypes, Login, Logout} from './auth.actions'; 4 | import {tap} from 'rxjs/operators'; 5 | import {Router} from '@angular/router'; 6 | import {defer, of} from 'rxjs'; 7 | 8 | 9 | @Injectable() 10 | export class AuthEffects { 11 | 12 | @Effect({dispatch:false}) 13 | login$ = this.actions$.pipe( 14 | ofType(AuthActionTypes.LoginAction), 15 | tap(action => localStorage.setItem("user", JSON.stringify(action.payload.user))) 16 | ); 17 | 18 | @Effect({dispatch:false}) 19 | logout$ = this.actions$.pipe( 20 | ofType(AuthActionTypes.LogoutAction), 21 | tap(() => { 22 | 23 | localStorage.removeItem("user"); 24 | this.router.navigateByUrl('/login'); 25 | 26 | }) 27 | ); 28 | 29 | @Effect() 30 | init$ = defer(() => { 31 | 32 | const userData = localStorage.getItem("user"); 33 | 34 | if (userData) { 35 | return of(new Login({user:JSON.parse(userData)})); 36 | } 37 | else { 38 | return of(new Logout()); 39 | } 40 | 41 | }); 42 | 43 | constructor(private actions$: Actions, private router:Router) { 44 | 45 | 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/app/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; 3 | import {Observable} from 'rxjs'; 4 | import {select, Store} from '@ngrx/store'; 5 | import {AppState} from '../reducers'; 6 | import {isLoggedIn} from './auth.selectors'; 7 | import {tap} from 'rxjs/operators'; 8 | 9 | 10 | 11 | @Injectable() 12 | export class AuthGuard implements CanActivate { 13 | 14 | 15 | constructor(private store: Store, private router: Router) { 16 | 17 | } 18 | 19 | 20 | canActivate(route: ActivatedRouteSnapshot, 21 | state: RouterStateSnapshot): Observable { 22 | 23 | return this.store 24 | .pipe( 25 | select(isLoggedIn), 26 | tap(loggedIn => { 27 | 28 | if (!loggedIn) { 29 | this.router.navigateByUrl('/login'); 30 | } 31 | 32 | }) 33 | ); 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {LoginComponent} from './login/login.component'; 4 | import {MatCardModule} from "@angular/material/card"; 5 | import { MatInputModule } from "@angular/material/input"; 6 | import {RouterModule} from "@angular/router"; 7 | import {ReactiveFormsModule} from "@angular/forms"; 8 | import {MatButtonModule} from "@angular/material/button"; 9 | import { StoreModule } from '@ngrx/store'; 10 | import {AuthService} from "./auth.service"; 11 | import * as fromAuth from './auth.reducer'; 12 | import {AuthGuard} from './auth.guard'; 13 | import { EffectsModule } from '@ngrx/effects'; 14 | import { AuthEffects } from './auth.effects'; 15 | 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | ReactiveFormsModule, 21 | MatCardModule, 22 | MatInputModule, 23 | MatButtonModule, 24 | RouterModule.forChild([{path: '', component: LoginComponent}]), 25 | StoreModule.forFeature('auth', fromAuth.authReducer), 26 | EffectsModule.forFeature([AuthEffects]), 27 | 28 | ], 29 | declarations: [LoginComponent], 30 | exports: [LoginComponent] 31 | }) 32 | export class AuthModule { 33 | static forRoot(): ModuleWithProviders { 34 | return { 35 | ngModule: AuthModule, 36 | providers: [ 37 | AuthService, 38 | AuthGuard 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/auth/auth.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { authReducer, initialAuthState } from './auth.reducer'; 2 | 3 | describe('Auth Reducer', () => { 4 | describe('unknown action', () => { 5 | it('should return the initial state', () => { 6 | const action = {} as any; 7 | 8 | const result = authReducer(initialAuthState, action); 9 | 10 | expect(result).toBe(initialAuthState); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/auth/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import {User} from '../model/user.model'; 3 | import {AuthActions, AuthActionTypes} from './auth.actions'; 4 | 5 | 6 | export interface AuthState { 7 | loggedIn: boolean, 8 | user: User 9 | } 10 | 11 | export const initialAuthState: AuthState = { 12 | loggedIn: false, 13 | user: undefined 14 | }; 15 | 16 | export function authReducer(state = initialAuthState, 17 | action: AuthActions): AuthState { 18 | switch (action.type) { 19 | 20 | case AuthActionTypes.LoginAction: 21 | return { 22 | loggedIn: true, 23 | user: action.payload.user 24 | }; 25 | 26 | case AuthActionTypes.LogoutAction: 27 | return { 28 | loggedIn: false, 29 | user: undefined 30 | }; 31 | 32 | default: 33 | return state; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/auth/auth.selectors.ts: -------------------------------------------------------------------------------- 1 | import {createSelector} from '@ngrx/store'; 2 | 3 | 4 | export const selectAuthState = state => state.auth; 5 | 6 | 7 | export const isLoggedIn = createSelector( 8 | selectAuthState, 9 | auth => auth.loggedIn 10 | ); 11 | 12 | 13 | export const isLoggedOut = createSelector( 14 | isLoggedIn, 15 | loggedIn => !loggedIn 16 | ); 17 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from "@angular/core"; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {Observable} from "rxjs"; 4 | import {User} from "../model/user.model"; 5 | 6 | 7 | 8 | 9 | @Injectable() 10 | export class AuthService { 11 | 12 | constructor(private http:HttpClient) { 13 | 14 | } 15 | 16 | login(email:string, password:string): Observable { 17 | return this.http.post('/api/login', {email,password}); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |

5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .login-page { 4 | max-width: 350px; 5 | margin: 50px auto 0 auto; 6 | } 7 | 8 | .login-form { 9 | display: flex; 10 | flex-direction: column; 11 | } -------------------------------------------------------------------------------- /src/app/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from "@angular/forms"; 3 | 4 | import {Store} from "@ngrx/store"; 5 | 6 | import {AuthService} from "../auth.service"; 7 | import {tap} from "rxjs/operators"; 8 | import {noop} from "rxjs"; 9 | import {Router} from "@angular/router"; 10 | import {AppState} from '../../reducers'; 11 | import {Login} from '../auth.actions'; 12 | 13 | @Component({ 14 | selector: 'login', 15 | templateUrl: './login.component.html', 16 | styleUrls: ['./login.component.scss'] 17 | }) 18 | export class LoginComponent implements OnInit { 19 | 20 | form: FormGroup; 21 | 22 | constructor( 23 | private fb:FormBuilder, 24 | private auth: AuthService, 25 | private router:Router, 26 | private store: Store) { 27 | 28 | this.form = fb.group({ 29 | email: ['test@angular-university.io', [Validators.required]], 30 | password: ['test', [Validators.required]] 31 | }); 32 | 33 | } 34 | 35 | ngOnInit() { 36 | 37 | } 38 | 39 | login() { 40 | 41 | const val = this.form.value; 42 | 43 | this.auth.login(val.email, val.password) 44 | .pipe( 45 | tap(user => { 46 | 47 | this.store.dispatch(new Login({user})); 48 | 49 | this.router.navigateByUrl('/courses'); 50 | 51 | }) 52 | ) 53 | .subscribe( 54 | noop, 55 | () => alert('Login Failed') 56 | ); 57 | 58 | 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/app/courses/course-dialog/course-dialog.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .mat-form-field { 4 | display: block; 5 | } 6 | 7 | textarea { 8 | height: 100px; 9 | resize: vertical; 10 | } -------------------------------------------------------------------------------- /src/app/courses/course-dialog/course-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 |

{{description}}

3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | Beginner 22 | 23 | Intermediate 24 | 25 | Advanced 26 | 27 | 28 | 29 | 30 | 31 | Promotion On 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/app/courses/course-dialog/course-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit, ViewEncapsulation} from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; 3 | import {FormBuilder, Validators, FormGroup} from "@angular/forms"; 4 | import * as moment from 'moment'; 5 | import {Course} from "../model/course"; 6 | import {CoursesService} from "../services/courses.service"; 7 | import {AppState} from "../../reducers"; 8 | import {Store} from "@ngrx/store"; 9 | import {Update} from "@ngrx/entity"; 10 | import {CourseSaved} from '../course.actions'; 11 | 12 | @Component({ 13 | selector: 'course-dialog', 14 | templateUrl: './course-dialog.component.html', 15 | styleUrls: ['./course-dialog.component.css'] 16 | }) 17 | export class CourseDialogComponent implements OnInit { 18 | 19 | courseId:number; 20 | 21 | form: FormGroup; 22 | description:string; 23 | 24 | constructor( 25 | private store: Store, 26 | private coursesService: CoursesService, 27 | private fb: FormBuilder, 28 | private dialogRef: MatDialogRef, 29 | @Inject(MAT_DIALOG_DATA) course:Course ) { 30 | 31 | this.courseId = course.id; 32 | 33 | this.description = course.description; 34 | 35 | 36 | this.form = fb.group({ 37 | description: [course.description, Validators.required], 38 | category: [course.category, Validators.required], 39 | longDescription: [course.longDescription,Validators.required], 40 | promo: [course.promo, []] 41 | }); 42 | 43 | } 44 | 45 | ngOnInit() { 46 | 47 | } 48 | 49 | 50 | save() { 51 | 52 | const changes = this.form.value; 53 | 54 | this.coursesService 55 | .saveCourse(this.courseId, changes) 56 | .subscribe( 57 | () => { 58 | 59 | const course: Update = { 60 | id: this.courseId, 61 | changes 62 | }; 63 | 64 | this.store.dispatch(new CourseSaved({course})); 65 | 66 | this.dialogRef.close(); 67 | } 68 | ); 69 | } 70 | 71 | close() { 72 | this.dialogRef.close(); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/app/courses/course.actions.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | import {Course} from './model/course'; 3 | import {Update} from '@ngrx/entity'; 4 | import {Lesson} from './model/lesson'; 5 | 6 | 7 | export enum CourseActionTypes { 8 | CourseRequested = '[View Course Page] Course Requested', 9 | CourseLoaded = '[Courses API] Course Loaded', 10 | AllCoursesRequested = '[Courses Home Page] All Courses Requested', 11 | AllCoursesLoaded = '[Courses API] All Courses Loaded', 12 | CourseSaved = '[Edit Course Dialog] Course Saved', 13 | LessonsPageRequested = '[Course Landing Page] Lessons Page Requested', 14 | LessonsPageLoaded = '[Courses API] Lessons Page Loaded', 15 | LessonsPageCancelled = '[Courses API] Lessons Page Cancelled' 16 | } 17 | 18 | export interface PageQuery { 19 | pageIndex: number; 20 | pageSize:number; 21 | } 22 | 23 | export class LessonsPageRequested implements Action { 24 | 25 | readonly type = CourseActionTypes.LessonsPageRequested; 26 | 27 | constructor(public payload: {courseId:number, page:PageQuery}) {} 28 | 29 | } 30 | 31 | export class LessonsPageLoaded implements Action { 32 | 33 | readonly type = CourseActionTypes.LessonsPageLoaded; 34 | 35 | constructor(public payload:{lessons: Lesson[]}) {} 36 | 37 | } 38 | 39 | export class LessonsPageCancelled implements Action { 40 | 41 | readonly type = CourseActionTypes.LessonsPageCancelled; 42 | 43 | } 44 | 45 | 46 | export class CourseRequested implements Action { 47 | 48 | readonly type = CourseActionTypes.CourseRequested; 49 | 50 | constructor(public payload: { courseId: number }) { 51 | 52 | } 53 | } 54 | 55 | 56 | export class CourseLoaded implements Action { 57 | 58 | readonly type = CourseActionTypes.CourseLoaded; 59 | 60 | constructor(public payload: { course: Course }) { 61 | } 62 | } 63 | 64 | 65 | export class AllCoursesRequested implements Action { 66 | 67 | readonly type = CourseActionTypes.AllCoursesRequested; 68 | 69 | } 70 | 71 | export class AllCoursesLoaded implements Action { 72 | 73 | readonly type = CourseActionTypes.AllCoursesLoaded; 74 | 75 | constructor(public payload: { courses: Course[] }) { 76 | 77 | } 78 | 79 | } 80 | 81 | export class CourseSaved implements Action { 82 | 83 | readonly type = CourseActionTypes.CourseSaved; 84 | 85 | constructor(public payload: { course: Update }) {} 86 | } 87 | 88 | 89 | 90 | 91 | export type CourseActions = 92 | CourseRequested 93 | | CourseLoaded 94 | | AllCoursesRequested 95 | | AllCoursesLoaded 96 | | CourseSaved 97 | | LessonsPageRequested 98 | | LessonsPageLoaded 99 | | LessonsPageCancelled; 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/app/courses/course.effects.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Actions, Effect, ofType} from '@ngrx/effects'; 3 | import { 4 | AllCoursesLoaded, 5 | AllCoursesRequested, 6 | CourseActionTypes, 7 | CourseLoaded, 8 | CourseRequested, LessonsPageCancelled, LessonsPageLoaded, 9 | LessonsPageRequested 10 | } from './course.actions'; 11 | import {throwError,of} from 'rxjs'; 12 | import {catchError, concatMap, exhaustMap, filter, map, mergeMap, withLatestFrom} from "rxjs/operators"; 13 | import {CoursesService} from './services/courses.service'; 14 | import {AppState} from '../reducers'; 15 | import {select, Store} from '@ngrx/store'; 16 | import {allCoursesLoaded} from './course.selectors'; 17 | 18 | @Injectable() 19 | export class CourseEffects { 20 | 21 | @Effect() 22 | loadCourse$ = this.actions$ 23 | .pipe( 24 | ofType(CourseActionTypes.CourseRequested), 25 | mergeMap(action => this.coursesService.findCourseById(action.payload.courseId)), 26 | map(course => new CourseLoaded({course})) 27 | 28 | ); 29 | 30 | @Effect() 31 | loadAllCourses$ = this.actions$ 32 | .pipe( 33 | ofType(CourseActionTypes.AllCoursesRequested), 34 | withLatestFrom(this.store.pipe(select(allCoursesLoaded))), 35 | filter(([action, allCoursesLoaded]) => !allCoursesLoaded), 36 | mergeMap(() => this.coursesService.findAllCourses()), 37 | map(courses => new AllCoursesLoaded({courses})) 38 | ); 39 | 40 | 41 | @Effect() 42 | loadLessonsPage$ = this.actions$ 43 | .pipe( 44 | ofType(CourseActionTypes.LessonsPageRequested), 45 | mergeMap(({payload}) => 46 | this.coursesService.findLessons(payload.courseId, 47 | payload.page.pageIndex, payload.page.pageSize) 48 | .pipe( 49 | catchError(err => { 50 | console.log('error loading a lessons page ', err); 51 | this.store.dispatch(new LessonsPageCancelled()); 52 | return of([]); 53 | }) 54 | ) 55 | 56 | ), 57 | map(lessons => new LessonsPageLoaded({lessons})) 58 | ); 59 | 60 | 61 | 62 | constructor(private actions$ :Actions, private coursesService: CoursesService, 63 | private store: Store) { 64 | 65 | } 66 | 67 | } 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/app/courses/course.reducers.ts: -------------------------------------------------------------------------------- 1 | import {Course} from './model/course'; 2 | import {createEntityAdapter, EntityAdapter, EntityState} from '@ngrx/entity'; 3 | import {CourseActions, CourseActionTypes} from './course.actions'; 4 | 5 | 6 | 7 | export interface CoursesState extends EntityState { 8 | 9 | allCoursesLoaded:boolean; 10 | 11 | } 12 | 13 | 14 | export const adapter : EntityAdapter = 15 | createEntityAdapter(); 16 | 17 | 18 | export const initialCoursesState: CoursesState = adapter.getInitialState({ 19 | allCoursesLoaded: false 20 | }); 21 | 22 | 23 | export function coursesReducer(state = initialCoursesState , action: CourseActions): CoursesState { 24 | 25 | switch(action.type) { 26 | 27 | case CourseActionTypes.CourseLoaded: 28 | 29 | return adapter.addOne(action.payload.course, state); 30 | 31 | case CourseActionTypes.AllCoursesLoaded: 32 | 33 | return adapter.addAll(action.payload.courses, {...state, allCoursesLoaded:true}); 34 | 35 | case CourseActionTypes.CourseSaved: 36 | 37 | return adapter.updateOne(action.payload.course,state); 38 | 39 | default: { 40 | 41 | return state; 42 | } 43 | 44 | } 45 | } 46 | 47 | 48 | export const { 49 | selectAll, 50 | selectEntities, 51 | selectIds, 52 | selectTotal 53 | 54 | } = adapter.getSelectors(); 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/app/courses/course.selectors.ts: -------------------------------------------------------------------------------- 1 | import {createFeatureSelector, createSelector} from '@ngrx/store'; 2 | import {CoursesState} from './course.reducers'; 3 | 4 | import * as fromCourse from './course.reducers'; 5 | 6 | import * as fromLesson from './lessons.reducers'; 7 | 8 | import {PageQuery} from './course.actions'; 9 | import {LessonsState} from './lessons.reducers'; 10 | 11 | export const selectCoursesState = createFeatureSelector("courses"); 12 | 13 | export const selectLessonsState = createFeatureSelector("lessons"); 14 | 15 | 16 | export const selectCourseById = (courseId:number) => createSelector( 17 | selectCoursesState, 18 | coursesState => coursesState.entities[courseId] 19 | ); 20 | 21 | 22 | export const selectAllCourses = createSelector( 23 | selectCoursesState, 24 | fromCourse.selectAll 25 | 26 | ); 27 | 28 | export const selectBeginnerCourses = createSelector( 29 | selectAllCourses, 30 | courses => courses.filter(course => course.category === 'BEGINNER') 31 | ); 32 | 33 | 34 | export const selectAdvancedCourses = createSelector( 35 | selectAllCourses, 36 | courses => courses.filter(course => course.category === 'ADVANCED') 37 | ); 38 | 39 | export const selectPromoTotal = createSelector( 40 | selectAllCourses, 41 | courses => courses.filter(course => course.promo).length 42 | ); 43 | 44 | 45 | export const allCoursesLoaded = createSelector( 46 | selectCoursesState, 47 | coursesState => coursesState.allCoursesLoaded 48 | ); 49 | 50 | 51 | export const selectAllLessons = createSelector( 52 | selectLessonsState, 53 | fromLesson.selectAll 54 | 55 | ); 56 | 57 | 58 | export const selectLessonsPage = (courseId:number, page:PageQuery) => createSelector( 59 | selectAllLessons, 60 | allLessons => { 61 | 62 | const start = page.pageIndex * page.pageSize, 63 | end = start + page.pageSize; 64 | 65 | return allLessons 66 | .filter(lesson => lesson.courseId == courseId) 67 | .slice(start, end); 68 | 69 | } 70 | 71 | ); 72 | 73 | 74 | export const selectLessonsLoading = createSelector( 75 | selectLessonsState, 76 | lessonsState => lessonsState.loading 77 | ); 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/app/courses/course/course.component.css: -------------------------------------------------------------------------------- 1 | 2 | .course { 3 | text-align: center; 4 | max-width: 390px; 5 | margin: 0 auto; 6 | } 7 | 8 | .course-thumbnail { 9 | width: 150px; 10 | margin: 20px auto 0 auto; 11 | display: block; 12 | } 13 | 14 | .description-cell { 15 | text-align: left; 16 | margin: 10px auto; 17 | } 18 | 19 | .duration-cell { 20 | text-align: center; 21 | } 22 | 23 | .duration-cell mat-icon { 24 | display: inline-block; 25 | vertical-align: middle; 26 | font-size: 20px; 27 | } 28 | 29 | .spinner-container { 30 | height: 340px; 31 | width: 390px; 32 | position: fixed; 33 | background: white; 34 | margin-top: 70px; 35 | z-index: 1; 36 | } 37 | 38 | .lessons-table { 39 | min-height: 360px; 40 | margin-top: 10px; 41 | } 42 | 43 | .spinner-container mat-spinner { 44 | margin: 95px auto 0 auto; 45 | } 46 | 47 | .action-toolbar { 48 | margin-top: 20px; 49 | } -------------------------------------------------------------------------------- /src/app/courses/course/course.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{course?.description}}

4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | # 20 | 21 | {{lesson.seqNo}} 22 | 23 | 24 | 25 | 26 | 27 | Description 28 | 29 | {{lesson.description}} 31 | 32 | 33 | 34 | 35 | 36 | 37 | Duration 38 | 39 | {{lesson.duration}} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/app/courses/course/course.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; 3 | import {ActivatedRoute} from "@angular/router"; 4 | import { MatPaginator } from "@angular/material/paginator"; 5 | import { MatSort } from "@angular/material/sort"; 6 | import { MatTableDataSource } from "@angular/material/table"; 7 | import {Course} from "../model/course"; 8 | import {CoursesService} from "../services/courses.service"; 9 | import {debounceTime, distinctUntilChanged, startWith, tap, delay} from 'rxjs/operators'; 10 | import {merge, fromEvent, Observable} from "rxjs"; 11 | import {LessonsDataSource} from "../services/lessons.datasource"; 12 | import {AppState} from '../../reducers'; 13 | import {select, Store} from '@ngrx/store'; 14 | import {PageQuery} from '../course.actions'; 15 | import {selectLessonsLoading} from '../course.selectors'; 16 | 17 | 18 | @Component({ 19 | selector: 'course', 20 | templateUrl: './course.component.html', 21 | styleUrls: ['./course.component.css'] 22 | }) 23 | export class CourseComponent implements OnInit, AfterViewInit { 24 | 25 | course:Course; 26 | 27 | dataSource: LessonsDataSource; 28 | 29 | displayedColumns = ["seqNo", "description", "duration"]; 30 | 31 | @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; 32 | 33 | loading$ : Observable; 34 | 35 | 36 | constructor(private route: ActivatedRoute, private store: Store) { 37 | 38 | } 39 | 40 | ngOnInit() { 41 | 42 | this.course = this.route.snapshot.data["course"]; 43 | 44 | this.loading$ = this.store.pipe(select(selectLessonsLoading)); 45 | 46 | this.dataSource = new LessonsDataSource(this.store); 47 | 48 | const initialPage: PageQuery = { 49 | pageIndex: 0, 50 | pageSize: 3 51 | }; 52 | 53 | this.dataSource.loadLessons(this.course.id, initialPage); 54 | 55 | } 56 | 57 | ngAfterViewInit() { 58 | 59 | this.paginator.page 60 | .pipe( 61 | tap(() => this.loadLessonsPage()) 62 | ) 63 | .subscribe(); 64 | 65 | 66 | } 67 | 68 | loadLessonsPage() { 69 | 70 | const newPage: PageQuery = { 71 | pageIndex: this.paginator.pageIndex, 72 | pageSize: this.paginator.pageSize 73 | }; 74 | 75 | this.dataSource.loadLessons(this.course.id, newPage); 76 | 77 | } 78 | 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/app/courses/courses-card-list/courses-card-list.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .course-card { 4 | margin: 20px 10px; 5 | } 6 | 7 | .course-actions { 8 | text-align: center; 9 | } -------------------------------------------------------------------------------- /src/app/courses/courses-card-list/courses-card-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{course.description}} 8 | 9 | 10 | 11 | 12 | 13 | 14 |

{{course.longDescription}}

15 |
16 | 17 | 18 | 19 | 22 | 23 | 27 | 28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /src/app/courses/courses-card-list/courses-card-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; 2 | import {Course} from "../model/course"; 3 | import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; 4 | import {CourseDialogComponent} from "../course-dialog/course-dialog.component"; 5 | 6 | @Component({ 7 | selector: 'courses-card-list', 8 | templateUrl: './courses-card-list.component.html', 9 | styleUrls: ['./courses-card-list.component.css'] 10 | }) 11 | export class CoursesCardListComponent implements OnInit { 12 | 13 | @Input() 14 | courses: Course[]; 15 | 16 | constructor(private dialog: MatDialog) { 17 | } 18 | 19 | ngOnInit() { 20 | 21 | } 22 | 23 | editCourse(course:Course) { 24 | 25 | const dialogConfig = new MatDialogConfig(); 26 | 27 | dialogConfig.disableClose = true; 28 | dialogConfig.autoFocus = true; 29 | dialogConfig.width = '400px'; 30 | 31 | dialogConfig.data = course; 32 | 33 | const dialogRef = this.dialog.open(CourseDialogComponent, 34 | dialogConfig); 35 | 36 | 37 | } 38 | 39 | } 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/app/courses/courses.module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {HomeComponent} from "./home/home.component"; 4 | import {CoursesCardListComponent} from "./courses-card-list/courses-card-list.component"; 5 | import {CourseDialogComponent} from "./course-dialog/course-dialog.component"; 6 | import {CourseResolver} from "./services/course.resolver"; 7 | import {CoursesService} from "./services/courses.service"; 8 | import {CourseComponent} from "./course/course.component"; 9 | import { MatDatepickerModule } from "@angular/material/datepicker"; 10 | import { MatDialogModule } from "@angular/material/dialog"; 11 | import { MatInputModule } from "@angular/material/input"; 12 | import { MatPaginatorModule } from "@angular/material/paginator"; 13 | import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; 14 | import { MatSelectModule } from "@angular/material/select"; 15 | import { MatSlideToggleModule } from "@angular/material/slide-toggle"; 16 | import { MatSortModule } from "@angular/material/sort"; 17 | import { MatTableModule } from "@angular/material/table"; 18 | import {MatTabsModule} from "@angular/material/tabs"; 19 | import {ReactiveFormsModule} from "@angular/forms"; 20 | import {MatMenuModule} from "@angular/material/menu"; 21 | import {MatMomentDateModule} from "@angular/material-moment-adapter"; 22 | import {MatCardModule} from "@angular/material/card"; 23 | import {MatButtonModule} from "@angular/material/button"; 24 | import {MatIconModule} from "@angular/material/icon"; 25 | import {RouterModule, Routes} from "@angular/router"; 26 | import { StoreModule } from '@ngrx/store'; 27 | import { EffectsModule } from '@ngrx/effects'; 28 | import {CourseEffects} from './course.effects'; 29 | import {coursesReducer} from './course.reducers'; 30 | import {lessonsReducer} from './lessons.reducers'; 31 | 32 | 33 | export const coursesRoutes: Routes = [ 34 | { 35 | path: "", 36 | component: HomeComponent 37 | 38 | }, 39 | { 40 | path: ':id', 41 | component: CourseComponent, 42 | resolve: { 43 | course: CourseResolver 44 | } 45 | } 46 | ]; 47 | 48 | 49 | 50 | @NgModule({ 51 | imports: [ 52 | CommonModule, 53 | MatButtonModule, 54 | MatIconModule, 55 | MatCardModule, 56 | MatTabsModule, 57 | MatInputModule, 58 | MatTableModule, 59 | MatPaginatorModule, 60 | MatSortModule, 61 | MatProgressSpinnerModule, 62 | MatSlideToggleModule, 63 | MatDialogModule, 64 | MatSelectModule, 65 | MatDatepickerModule, 66 | MatMomentDateModule, 67 | ReactiveFormsModule, 68 | RouterModule.forChild(coursesRoutes), 69 | StoreModule.forFeature('courses', coursesReducer), 70 | StoreModule.forFeature('lessons', lessonsReducer), 71 | EffectsModule.forFeature([CourseEffects]) 72 | ], 73 | declarations: [HomeComponent, CoursesCardListComponent, CourseDialogComponent, CourseComponent], 74 | exports: [HomeComponent, CoursesCardListComponent, CourseDialogComponent, CourseComponent], 75 | entryComponents: [CourseDialogComponent], 76 | providers: [ 77 | CoursesService, 78 | CourseResolver 79 | ] 80 | }) 81 | export class CoursesModule { 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/app/courses/home/home.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .title { 4 | text-align: center; 5 | } 6 | 7 | .courses-panel { 8 | max-width: 350px; 9 | margin: 0 auto; 10 | } 11 | 12 | 13 | .counters { 14 | display: flex; 15 | } 16 | 17 | .filler { 18 | flex: 1 1 auto; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/app/courses/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

All Courses

5 | 6 |
7 |

In Promo: {{promoTotal$ | async}}

8 |
9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | -------------------------------------------------------------------------------- /src/app/courses/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Course} from "../model/course"; 3 | import {Observable} from "rxjs"; 4 | import {filter, map, tap, withLatestFrom} from "rxjs/operators"; 5 | import {CoursesService} from "../services/courses.service"; 6 | import {AppState} from '../../reducers'; 7 | import {select, Store} from '@ngrx/store'; 8 | import {selectAdvancedCourses, selectAllCourses, selectBeginnerCourses, selectPromoTotal} from '../course.selectors'; 9 | import {AllCoursesRequested} from '../course.actions'; 10 | @Component({ 11 | selector: 'home', 12 | templateUrl: './home.component.html', 13 | styleUrls: ['./home.component.css'] 14 | }) 15 | export class HomeComponent implements OnInit { 16 | 17 | promoTotal$: Observable; 18 | 19 | beginnerCourses$: Observable; 20 | 21 | advancedCourses$: Observable; 22 | 23 | constructor(private store: Store) { 24 | 25 | } 26 | 27 | ngOnInit() { 28 | 29 | this.store.dispatch(new AllCoursesRequested()); 30 | 31 | this.beginnerCourses$ = this.store.pipe(select(selectBeginnerCourses)); 32 | 33 | this.advancedCourses$ = this.store.pipe(select(selectAdvancedCourses)); 34 | 35 | this.promoTotal$ = this.store.pipe(select(selectPromoTotal)); 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/app/courses/lessons.reducers.ts: -------------------------------------------------------------------------------- 1 | import {createEntityAdapter, EntityAdapter, EntityState} from '@ngrx/entity'; 2 | import {Lesson} from './model/lesson'; 3 | import {Course} from './model/course'; 4 | import {CourseActions, CourseActionTypes} from './course.actions'; 5 | 6 | 7 | 8 | export interface LessonsState extends EntityState { 9 | loading:boolean; 10 | } 11 | 12 | function sortByCourseAndSeqNo(l1: Lesson, l2:Lesson) { 13 | const compare = l1.courseId - l2.courseId; 14 | if (compare != 0) { 15 | return compare; 16 | } 17 | else { 18 | return l1.seqNo - l2.seqNo; 19 | } 20 | } 21 | 22 | export const adapter : EntityAdapter = 23 | createEntityAdapter({ 24 | sortComparer: sortByCourseAndSeqNo 25 | }); 26 | 27 | 28 | const initialLessonsState = adapter.getInitialState({ 29 | loading: false 30 | }); 31 | 32 | 33 | 34 | export function lessonsReducer(state = initialLessonsState, 35 | action: CourseActions): LessonsState { 36 | 37 | switch(action.type) { 38 | 39 | case CourseActionTypes.LessonsPageCancelled: 40 | 41 | return { 42 | ...state, 43 | loading:false 44 | }; 45 | 46 | case CourseActionTypes.LessonsPageRequested: 47 | return { 48 | ...state, 49 | loading:true 50 | }; 51 | 52 | case CourseActionTypes.LessonsPageLoaded: 53 | 54 | return adapter.addMany(action.payload.lessons, {...state, loading:false}); 55 | 56 | 57 | default: 58 | 59 | return state; 60 | 61 | } 62 | 63 | } 64 | 65 | 66 | 67 | export const { 68 | selectAll, 69 | selectEntities, 70 | selectIds, 71 | selectTotal 72 | 73 | } = adapter.getSelectors(); 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/app/courses/model/course.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface Course { 4 | id:number; 5 | description:string; 6 | iconUrl: string; 7 | courseListIcon: string; 8 | longDescription: string; 9 | category:string; 10 | lessonsCount:number; 11 | promo:boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/courses/model/lesson.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface Lesson { 4 | id: number; 5 | description: string; 6 | duration: string; 7 | seqNo: number; 8 | courseId: number; 9 | } -------------------------------------------------------------------------------- /src/app/courses/services/course.resolver.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import {Injectable} from "@angular/core"; 5 | import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router"; 6 | import {Course} from "../model/course"; 7 | import {Observable} from "rxjs"; 8 | import {CoursesService} from "./courses.service"; 9 | import {AppState} from "../../reducers"; 10 | import {select, Store} from "@ngrx/store"; 11 | import {filter, first, tap} from "rxjs/operators"; 12 | import {selectCourseById} from '../course.selectors'; 13 | import {CourseRequested} from '../course.actions'; 14 | 15 | 16 | 17 | @Injectable() 18 | export class CourseResolver implements Resolve { 19 | 20 | constructor( 21 | private coursesService:CoursesService, 22 | private store: Store) { 23 | 24 | } 25 | 26 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 27 | 28 | const courseId = route.params['id']; 29 | 30 | return this.store 31 | .pipe( 32 | select(selectCourseById(courseId)), 33 | tap(course => { 34 | if (!course) { 35 | this.store.dispatch(new CourseRequested({courseId})); 36 | } 37 | }), 38 | filter(course => !!course), 39 | first() 40 | ) 41 | 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/app/courses/services/courses.service.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import {Injectable} from "@angular/core"; 4 | import {HttpClient, HttpParams} from "@angular/common/http"; 5 | import {Observable} from "rxjs"; 6 | import {Course} from "../model/course"; 7 | import {map} from "rxjs/operators"; 8 | import {Lesson} from "../model/lesson"; 9 | 10 | 11 | @Injectable() 12 | export class CoursesService { 13 | 14 | constructor(private http:HttpClient) { 15 | 16 | } 17 | 18 | findCourseById(courseId: number): Observable { 19 | return this.http.get(`/api/courses/${courseId}`); 20 | } 21 | 22 | findAllCourses(): Observable { 23 | return this.http.get('/api/courses') 24 | .pipe( 25 | map(res => res['payload']) 26 | ); 27 | } 28 | 29 | findAllCourseLessons(courseId:number): Observable { 30 | return this.http.get('/api/lessons', { 31 | params: new HttpParams() 32 | .set('courseId', courseId.toString()) 33 | .set('pageNumber', "0") 34 | .set('pageSize', "1000") 35 | }).pipe( 36 | map(res => res["payload"]) 37 | ); 38 | } 39 | 40 | findLessons( 41 | courseId:number, 42 | pageNumber = 0, pageSize = 3): Observable { 43 | 44 | return this.http.get('/api/lessons', { 45 | params: new HttpParams() 46 | .set('courseId', courseId.toString()) 47 | .set('sortOrder', 'asc') 48 | .set('pageNumber', pageNumber.toString()) 49 | .set('pageSize', pageSize.toString()) 50 | }).pipe( 51 | map(res => res["payload"]) 52 | ); 53 | } 54 | 55 | 56 | saveCourse(courseId: number, changes: Partial) { 57 | return this.http.put('/api/courses/' + courseId, changes); 58 | } 59 | 60 | 61 | } -------------------------------------------------------------------------------- /src/app/courses/services/lessons.datasource.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import {CollectionViewer, DataSource} from "@angular/cdk/collections"; 5 | import {Observable, BehaviorSubject, of} from "rxjs"; 6 | import {Lesson} from "../model/lesson"; 7 | import {CoursesService} from "./courses.service"; 8 | import {catchError, finalize, tap} from 'rxjs/operators'; 9 | import {AppState} from '../../reducers'; 10 | import {select, Store} from '@ngrx/store'; 11 | import {LessonsPageRequested, PageQuery} from '../course.actions'; 12 | import {selectLessonsPage} from '../course.selectors'; 13 | 14 | 15 | 16 | export class LessonsDataSource implements DataSource { 17 | 18 | private lessonsSubject = new BehaviorSubject([]); 19 | 20 | constructor(private store: Store) { 21 | 22 | } 23 | 24 | loadLessons(courseId:number, page: PageQuery) { 25 | this.store 26 | .pipe( 27 | select(selectLessonsPage(courseId, page)), 28 | tap(lessons => { 29 | if (lessons.length > 0) { 30 | this.lessonsSubject.next(lessons); 31 | } 32 | else { 33 | this.store.dispatch(new LessonsPageRequested({courseId, page})); 34 | } 35 | }), 36 | catchError(() => of([])) 37 | ) 38 | .subscribe(); 39 | 40 | } 41 | 42 | connect(collectionViewer: CollectionViewer): Observable { 43 | console.log("Connecting data source"); 44 | return this.lessonsSubject.asObservable(); 45 | } 46 | 47 | disconnect(collectionViewer: CollectionViewer): void { 48 | this.lessonsSubject.complete(); 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/app/model/user.model.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface User { 4 | id: string; 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionReducer, 3 | ActionReducerMap, 4 | createFeatureSelector, 5 | createSelector, 6 | MetaReducer 7 | } from '@ngrx/store'; 8 | import { environment } from '../../environments/environment'; 9 | import {User} from '../model/user.model'; 10 | import {AuthActions, AuthActionTypes} from '../auth/auth.actions'; 11 | import {storeFreeze} from 'ngrx-store-freeze'; 12 | import {routerReducer} from '@ngrx/router-store'; 13 | 14 | 15 | export interface AppState { 16 | 17 | } 18 | 19 | export const reducers: ActionReducerMap = { 20 | router: routerReducer 21 | }; 22 | 23 | 24 | 25 | 26 | 27 | export const metaReducers: MetaReducer[] = 28 | !environment.production ? [storeFreeze] : []; 29 | -------------------------------------------------------------------------------- /src/app/shared/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StoreModule, ActionReducerMap } from '@ngrx/store'; 3 | import { Params, RouterStateSnapshot } from '@angular/router'; 4 | import { 5 | StoreRouterConnectingModule, 6 | routerReducer, 7 | RouterReducerState, 8 | RouterStateSerializer, 9 | } from '@ngrx/router-store'; 10 | 11 | export interface RouterStateUrl { 12 | url: string; 13 | params: Params; 14 | queryParams: Params; 15 | } 16 | 17 | export interface State { 18 | router: RouterReducerState; 19 | } 20 | 21 | export class CustomSerializer implements RouterStateSerializer { 22 | serialize(routerState: RouterStateSnapshot): RouterStateUrl { 23 | let route = routerState.root; 24 | 25 | while (route.firstChild) { 26 | route = route.firstChild; 27 | } 28 | 29 | const { url, root: { queryParams } } = routerState; 30 | const { params } = route; 31 | 32 | // Only return an object including the URL, params and query params 33 | // instead of the entire snapshot 34 | return { url, params, queryParams }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/ngrx-course-v7/56c83c8773bf37f21d1bc7062243c785770081c4/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/ngrx-course-v7/56c83c8773bf37f21d1bc7062243c785770081c4/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Angular Ngrx Course 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.log(err)); 15 | -------------------------------------------------------------------------------- /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/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | 68 | /** 69 | * Date, currency, decimal and percent pipes. 70 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 71 | */ 72 | // import 'intl'; // Run `npm install --save intl`. 73 | /** 74 | * Need to import at least one locale-data with intl. 75 | */ 76 | // import 'intl/locale-data/jsonp/en'; 77 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | 4 | 5 | @import "~@angular/material/theming"; 6 | 7 | // Include non-theme styles for core. 8 | @include mat-core(); 9 | 10 | $mat-custom-theme: ( 11 | 50: #e8f5e9, 12 | 100: #c8e6c9, 13 | 200: #a5d6a7, 14 | 300: #81c784, 15 | 400: #66bb6a, 16 | 500: #3dc046, 17 | 600: #43a047, 18 | 700: #388e3c, 19 | 800: #2e7d32, 20 | 900: #1b5e20, 21 | A100: #b9f6ca, 22 | A200: #69f0ae, 23 | A400: #00e676, 24 | A700: #00c853, 25 | contrast: ( 26 | 50: $dark-primary-text, 27 | 100: $dark-primary-text, 28 | 200: $dark-primary-text, 29 | 300: $dark-primary-text, 30 | 400: $dark-primary-text, 31 | 500: $light-primary-text, 32 | 600: $light-primary-text, 33 | 700: $light-primary-text, 34 | 800: $light-primary-text, 35 | 900: $light-primary-text, 36 | A100: $dark-primary-text, 37 | A200: $light-primary-text, 38 | A400: $light-primary-text, 39 | A700: $light-primary-text, 40 | ) 41 | ); 42 | 43 | // Define a theme. 44 | $primary: mat-palette($mat-blue-grey); 45 | $accent: mat-palette($mat-pink, A200, A100, A400); 46 | 47 | $theme: mat-light-theme($primary, $accent); 48 | 49 | // Include all theme styles for the components. 50 | @include angular-material-theme($theme); -------------------------------------------------------------------------------- /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 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es2015", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom", 18 | "ES2017.object" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs/Rx" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": true, 70 | "no-unnecessary-initializer": true, 71 | "no-unused-expression": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": true, 83 | "quotemark": [ 84 | true, 85 | "single" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "typeof-compare": true, 107 | "unified-signatures": true, 108 | "variable-name": false, 109 | "whitespace": [ 110 | true, 111 | "check-branch", 112 | "check-decl", 113 | "check-operator", 114 | "check-separator", 115 | "check-type" 116 | ], 117 | "directive-selector": [ 118 | true, 119 | "attribute", 120 | "app", 121 | "camelCase" 122 | ], 123 | "component-selector": [ 124 | true, 125 | "element", 126 | "app", 127 | "kebab-case" 128 | ], 129 | "no-inputs-metadata-property": true, 130 | "no-outputs-metadata-property": true, 131 | "no-host-metadata-property": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-lifecycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "component-class-suffix": true, 137 | "directive-class-suffix": true, 138 | "invoke-injectable": true 139 | } 140 | } 141 | --------------------------------------------------------------------------------