├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
2 | Login
3 |
4 |
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 |
--------------------------------------------------------------------------------