├── .gitignore ├── README.md ├── clientapp ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.e2e.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── backend.ts │ │ ├── filters │ │ │ ├── filters.component.css │ │ │ ├── filters.component.html │ │ │ └── filters.component.ts │ │ ├── format-rating.pipe.ts │ │ ├── model.ts │ │ ├── rate-button │ │ │ ├── rate-button.component.html │ │ │ └── rate-button.component.ts │ │ ├── talk-details │ │ │ ├── talk-details.component.css │ │ │ ├── talk-details.component.html │ │ │ └── talk-details.component.ts │ │ ├── talk │ │ │ ├── talk.component.css │ │ │ ├── talk.component.html │ │ │ └── talk.component.ts │ │ ├── talks-and-filters │ │ │ ├── talks-and-filters.component.css │ │ │ ├── talks-and-filters.component.html │ │ │ └── talks-and-filters.component.ts │ │ ├── talks │ │ │ ├── talks.component.css │ │ │ ├── talks.component.html │ │ │ └── talks.component.ts │ │ ├── watch-button │ │ │ ├── watch-button.component.html │ │ │ └── watch-button.component.ts │ │ └── watch.ts │ ├── assets │ │ ├── .gitkeep │ │ └── indigo-pink.css │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── typings.d.ts ├── tsconfig.json └── tslint.json └── server ├── package.json ├── server.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /client/dist 5 | /client/tmp 6 | /server/dist 7 | /server/tmp 8 | 9 | # dependencies 10 | /client/node_modules 11 | /server/node_modules 12 | 13 | # IDEs and editors 14 | /client/.idea 15 | /server/.idea 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage/* 34 | /libpeerconnection.log 35 | npm-debug.log 36 | testem.log 37 | /typings 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | #System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Application Showing How to Use NgRx 4 2 | 3 | This application illustrates how to use NgRx 4 to implement the state management of your application. 4 | 5 | It is based on this talk: 6 | https://www.youtube.com/watch?v=brCGZ8Lk-HY&t=1107s 7 | 8 | * The first commit shows an application with an ad-hoc state-management strategy (with a few issues). 9 | * The second commit fixes the issues in ad-hoc way. 10 | * The third commit is the refactoring introducing NgRx 4. 11 | 12 | ## Try It! 13 | 14 | * Go to `server` 15 | * Run `npm install` 16 | * Run `npm run server` 17 | * Go to `clientapp` 18 | * Run `npm install` 19 | * Run `ng serve` -------------------------------------------------------------------------------- /clientapp/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "clientapp" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico", 13 | "../node_modules/@angular/material/prebuilt-themes" 14 | ], 15 | "index": "index.html", 16 | "main": "main.ts", 17 | "polyfills": "polyfills.ts", 18 | "test": "test.ts", 19 | "tsconfig": "tsconfig.app.json", 20 | "testTsconfig": "tsconfig.spec.json", 21 | "prefix": "app", 22 | "styles": [ 23 | "styles.css" 24 | ], 25 | "scripts": [], 26 | "environmentSource": "environments/environment.ts", 27 | "environments": { 28 | "dev": "environments/environment.ts", 29 | "prod": "environments/environment.prod.ts" 30 | } 31 | } 32 | ], 33 | "e2e": { 34 | "protractor": { 35 | "config": "./protractor.conf.js" 36 | } 37 | }, 38 | "lint": [ 39 | { 40 | "project": "src/tsconfig.app.json" 41 | }, 42 | { 43 | "project": "src/tsconfig.spec.json" 44 | }, 45 | { 46 | "project": "e2e/tsconfig.e2e.json" 47 | } 48 | ], 49 | "test": { 50 | "karma": { 51 | "config": "./karma.conf.js" 52 | } 53 | }, 54 | "defaults": { 55 | "styleExt": "css", 56 | "component": {} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /clientapp/.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 | -------------------------------------------------------------------------------- /clientapp/.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 | -------------------------------------------------------------------------------- /clientapp/README.md: -------------------------------------------------------------------------------- 1 | # Clientapp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.1.1. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | Before running the tests make sure you are serving the app via `ng serve`. 25 | 26 | ## Further help 27 | 28 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 29 | -------------------------------------------------------------------------------- /clientapp/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientappPage } from './app.po'; 2 | 3 | describe('clientapp App', () => { 4 | let page: ClientappPage; 5 | 6 | beforeEach(() => { 7 | page = new ClientappPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /clientapp/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class ClientappPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /clientapp/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /clientapp/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | 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 | -------------------------------------------------------------------------------- /clientapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clientapp", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^4.0.0", 16 | "@angular/common": "^4.0.0", 17 | "@angular/compiler": "^4.0.0", 18 | "@angular/core": "^4.0.0", 19 | "@angular/forms": "^4.0.0", 20 | "@angular/http": "^4.0.0", 21 | "@angular/platform-browser": "^4.0.0", 22 | "@angular/platform-browser-dynamic": "^4.0.0", 23 | "@angular/router": "^4.0.0", 24 | "@angular/material": "2.0.0-beta.7", 25 | "core-js": "^2.4.1", 26 | "rxjs": "^5.1.0", 27 | "zone.js": "^0.8.4", 28 | "@ngrx/store": "github:ngrx/store-builds", 29 | "@ngrx/effects": "github:ngrx/effects-builds", 30 | "@ngrx/router-store": "github:ngrx/router-store-builds" 31 | 32 | }, 33 | "devDependencies": { 34 | "@angular/cli": "1.1.1", 35 | "@angular/compiler-cli": "^4.0.0", 36 | "@angular/language-service": "^4.0.0", 37 | "@types/jasmine": "2.5.45", 38 | "@types/node": "~6.0.60", 39 | "codelyzer": "~3.0.1", 40 | "jasmine-core": "~2.6.2", 41 | "jasmine-spec-reporter": "~4.1.0", 42 | "karma": "~1.7.0", 43 | "karma-chrome-launcher": "~2.1.1", 44 | "karma-cli": "~1.0.1", 45 | "karma-jasmine": "~1.1.0", 46 | "karma-jasmine-html-reporter": "^0.2.2", 47 | "karma-coverage-istanbul-reporter": "^1.2.1", 48 | "protractor": "~5.1.2", 49 | "ts-node": "~3.0.4", 50 | "tslint": "~5.3.2", 51 | "typescript": "~2.3.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /clientapp/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 | -------------------------------------------------------------------------------- /clientapp/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } -------------------------------------------------------------------------------- /clientapp/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

2 | Tech Talks 3 |

4 | 5 | -------------------------------------------------------------------------------- /clientapp/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 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 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /clientapp/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'app'; 10 | } 11 | -------------------------------------------------------------------------------- /clientapp/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 { WatchButtonComponent } from './watch-button/watch-button.component'; 6 | import { TalksAndFiltersComponent } from './talks-and-filters/talks-and-filters.component'; 7 | import { TalksComponent } from './talks/talks.component'; 8 | import { TalkDetailsComponent } from './talk-details/talk-details.component'; 9 | import { TalkComponent } from './talk/talk.component'; 10 | import { RateButtonComponent } from './rate-button/rate-button.component'; 11 | import { FormatRatingPipe } from './format-rating.pipe'; 12 | import { FiltersComponent } from './filters/filters.component'; 13 | import { ReactiveFormsModule } from '@angular/forms'; 14 | import { HttpModule } from '@angular/http'; 15 | import { RouterModule } from '@angular/router'; 16 | import { MaterialModule, MdInputModule, MdCheckboxModule } from '@angular/material'; 17 | import { Backend } from "./backend"; 18 | import { WatchService } from "./watch"; 19 | import { appReducer, initialState, State, TalksEffects } from './model'; 20 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 21 | import { StoreModule, ActionReducer, combineReducers } from '@ngrx/store'; 22 | import { EffectsModule } from "@ngrx/effects"; 23 | import { StoreRouterConnectingModule } from "@ngrx/router-store"; 24 | 25 | @NgModule({ 26 | declarations: [ 27 | AppComponent, 28 | WatchButtonComponent, 29 | TalksAndFiltersComponent, 30 | TalksComponent, 31 | TalkDetailsComponent, 32 | TalkComponent, 33 | RateButtonComponent, 34 | FormatRatingPipe, 35 | FiltersComponent 36 | ], 37 | imports: [ 38 | BrowserModule, 39 | ReactiveFormsModule, 40 | HttpModule, 41 | NoopAnimationsModule, 42 | 43 | MaterialModule, 44 | MdInputModule, 45 | MdCheckboxModule, 46 | 47 | RouterModule.forRoot([ 48 | { path: '', pathMatch: 'full', redirectTo: 'talks' }, 49 | { path: 'talks', pathMatch: 'full', component: TalksAndFiltersComponent }, 50 | { path: 'talk/:id', component: TalkDetailsComponent } 51 | ], {useHash: true}), 52 | 53 | StoreModule.forRoot({app: appReducer}, {initialState}), 54 | 55 | EffectsModule.forRoot([ 56 | TalksEffects 57 | ]), 58 | 59 | StoreRouterConnectingModule 60 | ], 61 | providers: [ 62 | Backend, 63 | WatchService, 64 | TalksEffects 65 | ], 66 | bootstrap: [AppComponent] 67 | }) 68 | export class AppModule { } 69 | -------------------------------------------------------------------------------- /clientapp/src/app/backend.ts: -------------------------------------------------------------------------------- 1 | import {Http, URLSearchParams} from "@angular/http"; 2 | import {Observable} from "rxjs/Observable"; 3 | import {Injectable} from "@angular/core"; 4 | import {Filters, Talk} from "./model"; 5 | 6 | @Injectable() 7 | export class Backend { 8 | private url = 'http://localhost:4444'; 9 | 10 | constructor(private http: Http) {} 11 | 12 | findTalks(filters: Filters): Observable<{talks: {[id: number]: Talk}, list: number[]}> { 13 | const params = new URLSearchParams(); 14 | params.set("speaker", filters.speaker); 15 | params.set("title", filters.title); 16 | params.set("minRating", filters.minRating.toString()); 17 | return this.http.get(`${this.url}/talks`, {search: params}).map(r => r.json()); 18 | } 19 | 20 | findTalk(id: number): Observable { 21 | const params = new URLSearchParams(); 22 | params.set("id", id.toString()); 23 | return this.http.get(`${this.url}/talk/`, {search: params}).map(r => r.json()['talk']); 24 | } 25 | 26 | rateTalk(id: number, rating: number): Observable { 27 | return this.http.post(`${this.url}/rate`, {id, yourRating: rating}); 28 | } 29 | } -------------------------------------------------------------------------------- /clientapp/src/app/filters/filters.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | padding: 10px; 3 | display: flex; 4 | } 5 | 6 | div { 7 | display: flex; 8 | flex-direction: column; 9 | flex-grow: 1; 10 | } 11 | 12 | md-input-container { 13 | flex-grow: 1; 14 | } 15 | 16 | md-checkbox { 17 | flex-grow: 1; 18 | } -------------------------------------------------------------------------------- /clientapp/src/app/filters/filters.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | High Rating 12 | 13 |
-------------------------------------------------------------------------------- /clientapp/src/app/filters/filters.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Output, Inject, Input} from "@angular/core"; 2 | import {FormGroup, FormControl} from "@angular/forms"; 3 | import 'rxjs/add/operator/debounceTime'; 4 | import {Filters} from "../model"; 5 | 6 | 7 | @Component({ 8 | selector: 'filters-cmp', 9 | templateUrl: './filters.component.html' 10 | }) 11 | export class FiltersComponent { 12 | @Output() filtersChange = new EventEmitter(); 13 | 14 | @Input() set filters(v) { 15 | this.filtersForm.setValue({ 16 | title: v.title, 17 | speaker: v.speaker, 18 | highRating: v.minRating >= 9 19 | }, {emitEvent: false}); 20 | } 21 | 22 | filtersForm = new FormGroup({ 23 | speaker: new FormControl(), 24 | title: new FormControl(), 25 | highRating: new FormControl(false), 26 | }); 27 | 28 | constructor() { 29 | this.filtersForm.valueChanges.debounceTime(200).subscribe((value) => { 30 | this.filtersChange.next(this.createFiltersObject(value)); 31 | }); 32 | } 33 | 34 | private createFiltersObject({title, speaker, highRating}: { title: string, speaker: string, highRating: false }): Filters { 35 | const minRating = highRating ? 9 : 0; 36 | return {speaker: speaker || null, title: title || null, minRating}; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /clientapp/src/app/format-rating.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'formatRating' 5 | }) 6 | export class FormatRatingPipe implements PipeTransform { 7 | transform(value: any, args?: any): any { 8 | return value; 9 | } 10 | } -------------------------------------------------------------------------------- /clientapp/src/app/model.ts: -------------------------------------------------------------------------------- 1 | import {RouterAction, ROUTER_NAVIGATION, RouterNavigationAction} from '@ngrx/router-store'; 2 | import {Actions, Effect} from '@ngrx/effects'; 3 | import {WatchService} from "app/watch"; 4 | import {Backend} from "app/backend"; 5 | import {Params, ActivatedRouteSnapshot} from "@angular/router"; 6 | import {Store, combineReducers} from "@ngrx/store"; 7 | import {Injectable} from "@angular/core"; 8 | import {of} from "rxjs/observable/of"; 9 | import {Observable} from "rxjs/Observable"; 10 | import 'rxjs/add/operator/withLatestFrom'; 11 | 12 | // state 13 | export type Talk = { id: number, title: string, speaker: string, description: string, yourRating: number, rating: number }; 14 | export type Filters = { speaker: string, title: string, minRating: number }; 15 | export type AppState = { talks: { [id: number]: Talk }, list: number[], filters: Filters, watched: { [id: number]: boolean } }; 16 | export type State = { app: AppState }; // this will also contain router state 17 | 18 | export const initialState: State = { 19 | app: { 20 | filters: {speaker: "", title: "", minRating: 0}, 21 | talks: {}, 22 | list: [], 23 | watched: {} 24 | } 25 | }; 26 | 27 | // actions 28 | export type TalksUpdated = { type: 'TALKS_UPDATED', payload: { talks: { [id: number]: Talk }, list: number[] }, filters: Filters }; 29 | export type TalkUpdated = { type: 'TALK_UPDATED', payload: Talk }; 30 | export type Watch = { type: 'WATCH', payload: { talkId: number } }; 31 | export type TalkWatched = { type: 'TALK_WATCHED', payload: { talkId: number } }; 32 | export type Rate = { type: 'RATE', payload: { talkId: number, rating: number } }; 33 | export type Unrate = { type: 'UNRATE', payload: { talkId: number, error: any } }; 34 | type Action = RouterAction | TalksUpdated | TalkUpdated | Watch | TalkWatched | Rate | Unrate; 35 | 36 | // reducer 37 | export function appReducer(state: AppState, action: Action): AppState { 38 | switch (action.type) { 39 | case 'TALKS_UPDATED': { 40 | return {...state, ...action.payload}; 41 | } 42 | case 'TALK_UPDATED': { 43 | const talks = {...state.talks}; 44 | talks[action.payload.id] = action.payload; 45 | return {...state, talks}; 46 | } 47 | case 'RATE': { 48 | const talks = {...state.talks}; 49 | talks[action.payload.talkId].rating = action.payload.rating; 50 | return {...state, talks}; 51 | } 52 | case 'UNRATE': { 53 | const talks = {...state.talks}; 54 | talks[action.payload.talkId].rating = null; 55 | return {...state, talks}; 56 | } 57 | case 'TALK_WATCHED': { 58 | const watched = {...state.watched}; 59 | watched[action.payload.talkId] = true; 60 | return {...state, watched}; 61 | } 62 | default: { 63 | return state; 64 | } 65 | } 66 | } 67 | 68 | @Injectable() 69 | export class TalksEffects { 70 | @Effect() navigateToTalks = this.handleNavigation('talks', (r: ActivatedRouteSnapshot) => { 71 | const filters = createFilters(r.params); 72 | return this.backend.findTalks(filters).map(resp => ({type: 'TALKS_UPDATED', payload: {...resp, filters}})); 73 | }); 74 | 75 | @Effect() navigateToTalk = this.handleNavigation('talk/:id', (r: ActivatedRouteSnapshot, state: State) => { 76 | const id = +r.paramMap.get('id'); 77 | if (! state.app.talks[id]) { 78 | return this.backend.findTalk(+r.paramMap.get('id')).map(resp => ({type: 'TALK_UPDATED', payload: resp})); 79 | } else { 80 | return of(); 81 | } 82 | }); 83 | 84 | @Effect() rateTalk = this.actions.ofType('RATE'). 85 | switchMap((a: Rate) => { 86 | return this.backend.rateTalk(a.payload.talkId, a.payload.rating).switchMap(() => of()).catch(e => { 87 | console.log('Error', e); 88 | return of({type: 'UNRATE', payload: {talkId: a.payload.talkId}}); 89 | }); 90 | }); 91 | 92 | @Effect() watchTalk = this.actions.ofType('WATCH'). 93 | map((a: Watch) => { 94 | this.watch.watch(a.payload.talkId); 95 | return {type: 'TALK_WATCHED', payload: a.payload}; 96 | }); 97 | 98 | constructor(private actions: Actions, private store: Store, private backend: Backend, private watch: WatchService) { 99 | } 100 | 101 | private handleNavigation(segment: string, callback: (a: ActivatedRouteSnapshot, state: State) => Observable) { 102 | const nav = this.actions.ofType(ROUTER_NAVIGATION). 103 | map(firstSegment). 104 | filter(s => s.routeConfig.path === segment); 105 | 106 | return nav.withLatestFrom(this.store).switchMap(a => callback(a[0], a[1])).catch(e => { 107 | console.log('Network error', e); 108 | return of(); 109 | }); 110 | } 111 | } 112 | 113 | 114 | function firstSegment(r: RouterNavigationAction) { 115 | return r.payload.routerState.root.firstChild; 116 | } 117 | 118 | 119 | function createFilters(p: Params): Filters { 120 | return {speaker: p['speaker'] || null, title: p['title'] || null, minRating: p['minRating'] ? +p['minRating'] : 0}; 121 | } 122 | -------------------------------------------------------------------------------- /clientapp/src/app/rate-button/rate-button.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientapp/src/app/rate-button/rate-button.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, EventEmitter, Output} from '@angular/core'; 2 | import {Talk} from "../model"; 3 | 4 | @Component({ 5 | selector: 'rate-button', 6 | templateUrl: './rate-button.component.html' 7 | }) 8 | export class RateButtonComponent { 9 | @Input() talk: Talk; 10 | @Output() rate = new EventEmitter(); 11 | 12 | promptRating(): void { 13 | const value = prompt("Enter rating"); 14 | if (value) { 15 | this.rate.next(+value); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /clientapp/src/app/talk-details/talk-details.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | margin: 10px; 3 | } 4 | 5 | md-card { 6 | display: flex; 7 | } 8 | 9 | #rating-column { 10 | margin-right: 20px; 11 | } 12 | 13 | #rating { 14 | display: block; 15 | font-size: 25px; 16 | } 17 | 18 | #title { 19 | display: block; 20 | font-size: 20px; 21 | } 22 | 23 | #speaker { 24 | display: block; 25 | font-size: 15px; 26 | margin-bottom: 20px; 27 | } 28 | 29 | #description { 30 | margin-bottom: 20px; 31 | } -------------------------------------------------------------------------------- /clientapp/src/app/talk-details/talk-details.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Rating 4 | {{talk.rating | formatRating}} 5 |
6 | 7 |
8 | {{talk.title}} 9 | {{talk.speaker}} 10 | 11 |
12 | {{talk.description}} 13 |
14 | 15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /clientapp/src/app/talk-details/talk-details.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | import {Backend} from "../backend"; 3 | import {ActivatedRoute} from "@angular/router"; 4 | import 'rxjs/add/operator/mergeMap'; 5 | import {WatchService} from "../watch"; 6 | import { Talk, State } from "../model"; 7 | import { Store } from "@ngrx/store"; 8 | 9 | @Component({ 10 | selector: 'talk-details-cmp', 11 | templateUrl: './talk-details.component.html', 12 | styleUrls: ['./talk-details.component.css'] 13 | }) 14 | export class TalkDetailsComponent { 15 | talk: Talk; 16 | isWatched: boolean; 17 | 18 | constructor(private route: ActivatedRoute, private store: Store) { 19 | store.select('app').subscribe(t => { 20 | const id = (+route.snapshot.paramMap.get('id')); 21 | this.talk = t.talks[id]; 22 | this.isWatched = t.watched[id]; 23 | }); 24 | } 25 | 26 | handleRate(newRating: number): void { 27 | this.store.dispatch({ 28 | type: 'RATE', 29 | payload: { 30 | talkId: this.talk.id, 31 | rating: newRating 32 | } 33 | }); 34 | } 35 | 36 | handleWatch(): void { 37 | this.store.dispatch({ 38 | type: 'WATCH', 39 | payload: { 40 | talkId: this.talk.id, 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /clientapp/src/app/talk/talk.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | margin: 10px; 3 | } 4 | 5 | md-card { 6 | display: flex; 7 | } 8 | 9 | #rating-column { 10 | margin-right: 20px; 11 | } 12 | 13 | #rating { 14 | display: block; 15 | font-size: 25px; 16 | } 17 | 18 | #title { 19 | display: block; 20 | font-size: 20px; 21 | } 22 | 23 | #speaker { 24 | display: block; 25 | font-size: 15px; 26 | margin-bottom: 20px; 27 | } -------------------------------------------------------------------------------- /clientapp/src/app/talk/talk.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Rating 4 | {{talk.rating | formatRating}} 5 |
6 | 7 |
8 | {{talk.title}} 9 | {{talk.speaker}} 10 |
11 |
-------------------------------------------------------------------------------- /clientapp/src/app/talk/talk.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | import {Talk} from "../model"; 3 | 4 | @Component({ 5 | selector: 'talk-cmp', 6 | templateUrl: './talk.component.html', 7 | styleUrls: ['./talk.component.css'] 8 | }) 9 | export class TalkComponent { 10 | @Input() talk: Talk; 11 | } -------------------------------------------------------------------------------- /clientapp/src/app/talks-and-filters/talks-and-filters.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } -------------------------------------------------------------------------------- /clientapp/src/app/talks-and-filters/talks-and-filters.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /clientapp/src/app/talks-and-filters/talks-and-filters.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject} from "@angular/core"; 2 | import { Router, Params } from "@angular/router"; 3 | import { Filters, State, Talk } from "../model"; 4 | import { Store } from "@ngrx/store"; 5 | import { Observable } from "rxjs/Observable"; 6 | 7 | @Component({ 8 | selector: 'app-cmp', 9 | templateUrl: './talks-and-filters.component.html', 10 | styleUrls: ['./talks-and-filters.component.css'] 11 | }) 12 | export class TalksAndFiltersComponent { 13 | filters: Observable; 14 | talks: Observable; 15 | 16 | constructor(private router: Router, store: Store) { 17 | this.filters = store.select('app', 'filters'); 18 | this.talks = store.select('app').map(s => s.list.map(n => s.talks[n])); 19 | } 20 | 21 | handleFiltersChange(filters: Filters): void { 22 | this.router.navigate(["/talks", this.createParams(filters)]); 23 | } 24 | 25 | private createParams(filters: Filters): Params { 26 | const r: any = {}; 27 | if (filters.speaker) r.speaker = filters.speaker; 28 | if (filters.title) r.title = filters.title; 29 | if (filters.minRating) r.minRating = filters.minRating; 30 | return r; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /clientapp/src/app/talks/talks.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } -------------------------------------------------------------------------------- /clientapp/src/app/talks/talks.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientapp/src/app/talks/talks.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import {Talk} from "../model"; 3 | 4 | @Component({ 5 | selector: 'talks-cmp', 6 | templateUrl: './talks.component.html', 7 | styleUrls: ['./talks.component.css'] 8 | }) 9 | export class TalksComponent { 10 | @Input() talks: Talk[]; 11 | } -------------------------------------------------------------------------------- /clientapp/src/app/watch-button/watch-button.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clientapp/src/app/watch-button/watch-button.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, EventEmitter, Output} from '@angular/core'; 2 | import {Talk} from "../model"; 3 | 4 | @Component({ 5 | selector: 'watch-button', 6 | templateUrl: './watch-button.component.html' 7 | }) 8 | export class WatchButtonComponent { 9 | @Input() talk: Talk; 10 | @Input() watched: boolean; 11 | @Output() watch = new EventEmitter(); 12 | 13 | handleWatch(): void { 14 | this.watch.next(null); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /clientapp/src/app/watch.ts: -------------------------------------------------------------------------------- 1 | export class WatchService { 2 | watch(talkId: number): void { 3 | console.log("watch", talkId); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /clientapp/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsavkin/state_management_ngrx4/639114adcdbbdc57ee4f747792b22d8f48e03dad/clientapp/src/assets/.gitkeep -------------------------------------------------------------------------------- /clientapp/src/assets/indigo-pink.css: -------------------------------------------------------------------------------- 1 | .mat-elevation-z0{box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12)}.mat-elevation-z1{box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12)}.mat-elevation-z2{box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}.mat-elevation-z3{box-shadow:0 3px 3px -2px rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 1px 8px 0 rgba(0,0,0,.12)}.mat-elevation-z4{box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.mat-elevation-z5{box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 5px 8px 0 rgba(0,0,0,.14),0 1px 14px 0 rgba(0,0,0,.12)}.mat-elevation-z6{box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12)}.mat-elevation-z7{box-shadow:0 4px 5px -2px rgba(0,0,0,.2),0 7px 10px 1px rgba(0,0,0,.14),0 2px 16px 1px rgba(0,0,0,.12)}.mat-elevation-z8{box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12)}.mat-elevation-z9{box-shadow:0 5px 6px -3px rgba(0,0,0,.2),0 9px 12px 1px rgba(0,0,0,.14),0 3px 16px 2px rgba(0,0,0,.12)}.mat-elevation-z10{box-shadow:0 6px 6px -3px rgba(0,0,0,.2),0 10px 14px 1px rgba(0,0,0,.14),0 4px 18px 3px rgba(0,0,0,.12)}.mat-elevation-z11{box-shadow:0 6px 7px -4px rgba(0,0,0,.2),0 11px 15px 1px rgba(0,0,0,.14),0 4px 20px 3px rgba(0,0,0,.12)}.mat-elevation-z12{box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 12px 17px 2px rgba(0,0,0,.14),0 5px 22px 4px rgba(0,0,0,.12)}.mat-elevation-z13{box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 13px 19px 2px rgba(0,0,0,.14),0 5px 24px 4px rgba(0,0,0,.12)}.mat-elevation-z14{box-shadow:0 7px 9px -4px rgba(0,0,0,.2),0 14px 21px 2px rgba(0,0,0,.14),0 5px 26px 4px rgba(0,0,0,.12)}.mat-elevation-z15{box-shadow:0 8px 9px -5px rgba(0,0,0,.2),0 15px 22px 2px rgba(0,0,0,.14),0 6px 28px 5px rgba(0,0,0,.12)}.mat-elevation-z16{box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12)}.mat-elevation-z17{box-shadow:0 8px 11px -5px rgba(0,0,0,.2),0 17px 26px 2px rgba(0,0,0,.14),0 6px 32px 5px rgba(0,0,0,.12)}.mat-elevation-z18{box-shadow:0 9px 11px -5px rgba(0,0,0,.2),0 18px 28px 2px rgba(0,0,0,.14),0 7px 34px 6px rgba(0,0,0,.12)}.mat-elevation-z19{box-shadow:0 9px 12px -6px rgba(0,0,0,.2),0 19px 29px 2px rgba(0,0,0,.14),0 7px 36px 6px rgba(0,0,0,.12)}.mat-elevation-z20{box-shadow:0 10px 13px -6px rgba(0,0,0,.2),0 20px 31px 3px rgba(0,0,0,.14),0 8px 38px 7px rgba(0,0,0,.12)}.mat-elevation-z21{box-shadow:0 10px 13px -6px rgba(0,0,0,.2),0 21px 33px 3px rgba(0,0,0,.14),0 8px 40px 7px rgba(0,0,0,.12)}.mat-elevation-z22{box-shadow:0 10px 14px -6px rgba(0,0,0,.2),0 22px 35px 3px rgba(0,0,0,.14),0 8px 42px 7px rgba(0,0,0,.12)}.mat-elevation-z23{box-shadow:0 11px 14px -7px rgba(0,0,0,.2),0 23px 36px 3px rgba(0,0,0,.14),0 9px 44px 8px rgba(0,0,0,.12)}.mat-elevation-z24{box-shadow:0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12)}.mat-h1,.mat-headline,.mat-typography h1{font:400 24px/32px Roboto,"Helvetica Neue",sans-serif;margin:0 0 16px}.mat-h2,.mat-title,.mat-typography h2{font:500 20px/32px Roboto,"Helvetica Neue",sans-serif;margin:0 0 16px}.mat-h3,.mat-subheading-2,.mat-typography h3{font:400 16px/28px Roboto,"Helvetica Neue",sans-serif;margin:0 0 16px}.mat-h4,.mat-subheading-1,.mat-typography h4{font:400 15px/24px Roboto,"Helvetica Neue",sans-serif;margin:0 0 16px}.mat-h5,.mat-typography h5{font-size:11.62px;font-weight:400;font-family:Roboto,"Helvetica Neue",sans-serif;line-height:20px;margin:0 0 12px}.mat-h6,.mat-typography h6{font-size:9.38px;font-weight:400;font-family:Roboto,"Helvetica Neue",sans-serif;line-height:20px;margin:0 0 12px}.mat-body-2,.mat-body-strong{font:500 14px/24px Roboto,"Helvetica Neue",sans-serif}.mat-body,.mat-body-1,.mat-typography{font:400 14px/20px Roboto,"Helvetica Neue",sans-serif}.mat-body p,.mat-body-1 p,.mat-typography p{margin:0 0 12px}.mat-caption,.mat-small{font:400 12px/20px Roboto,"Helvetica Neue",sans-serif}.mat-display-4,.mat-typography .mat-display-4{font:300 112px/112px Roboto,"Helvetica Neue",sans-serif;margin:0 0 56px;letter-spacing:-.05em}.mat-display-3,.mat-typography .mat-display-3{font:400 56px/56px Roboto,"Helvetica Neue",sans-serif;margin:0 0 64px;letter-spacing:-.02em}.mat-display-2,.mat-typography .mat-display-2{font:400 45px/48px Roboto,"Helvetica Neue",sans-serif;margin:0 0 64px;letter-spacing:-.005em}.mat-display-1,.mat-typography .mat-display-1{font:400 34px/40px Roboto,"Helvetica Neue",sans-serif;margin:0 0 64px}.mat-button,.mat-icon-button,.mat-raised-button{font-family:Roboto,"Helvetica Neue",sans-serif;font-size:14px;font-weight:500}.mat-button-toggle{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-card{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-card-title{font-size:24px;font-weight:400}.mat-card-content,.mat-card-header .mat-card-title,.mat-card-subtitle{font-size:14px}.mat-checkbox{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-checkbox-layout .mat-checkbox-label{line-height:24px}.mat-chip:not(.mat-basic-chip){font-size:13px;line-height:16px}.mat-calendar{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-calendar-body{font-size:13px}.mat-calendar-body-label,.mat-calendar-period-button{font-size:14px;font-weight:500}.mat-calendar-table-header th{font-size:11px;font-weight:400}.mat-dialog-title{font:500 20px/32px Roboto,"Helvetica Neue",sans-serif}.mat-grid-tile-footer,.mat-grid-tile-header{font-size:14px}.mat-grid-tile-footer .mat-line,.mat-grid-tile-header .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-grid-tile-footer .mat-line:nth-child(n+2),.mat-grid-tile-header .mat-line:nth-child(n+2){font-size:12px}.mat-input-container{font:400 inherit/1.125 Roboto,"Helvetica Neue",sans-serif}.mat-input-wrapper{padding-bottom:1.29688em}.mat-input-prefix .mat-datepicker-toggle,.mat-input-prefix .mat-icon,.mat-input-suffix .mat-datepicker-toggle,.mat-input-suffix .mat-icon{font-size:150%}.mat-input-prefix .mat-icon-button,.mat-input-suffix .mat-icon-button{height:1.5em;width:1.5em}.mat-input-prefix .mat-icon-button .mat-icon,.mat-input-suffix .mat-icon-button .mat-icon{line-height:1.5}.mat-input-infix{padding:.4375em 0;border-top:.84375em solid transparent}.mat-input-element:-webkit-autofill+.mat-input-placeholder-wrapper .mat-float{transform:translateY(-1.28125em) scale(.75) perspective(100px) translateZ(.001px);-ms-transform:translateY(-1.28125em) scale(.75);width:133.33333%}.mat-input-placeholder-wrapper{top:-.84375em;padding-top:.84375em}.mat-input-placeholder{top:1.28125em}.mat-focused .mat-input-placeholder.mat-float,.mat-input-placeholder.mat-float:not(.mat-empty){transform:translateY(-1.28125em) scale(.75) perspective(100px) translateZ(.001px);-ms-transform:translateY(-1.28125em) scale(.75);width:133.33333%}.mat-input-underline{bottom:1.29688em}.mat-input-subscript-wrapper{font-size:75%;margin-top:.60417em;top:calc(100% - 1.72917em)}.mat-menu-item{font-family:Roboto,"Helvetica Neue",sans-serif;font-size:16px}.mat-radio-button{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-select{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-select-trigger{font-size:16px}.mat-slide-toggle-content{font:400 14px/20px Roboto,"Helvetica Neue",sans-serif}.mat-slider-thumb-label-text{font-size:12px;font-weight:500}.mat-tab-group{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-tab-label,.mat-tab-link{font-family:Roboto,"Helvetica Neue",sans-serif;font-size:14px;font-weight:500}.mat-toolbar,.mat-toolbar h1,.mat-toolbar h2,.mat-toolbar h3,.mat-toolbar h4,.mat-toolbar h5,.mat-toolbar h6{font:500 20px/32px Roboto,"Helvetica Neue",sans-serif;margin:0}.mat-tooltip{font-family:Roboto,"Helvetica Neue",sans-serif;font-size:10px;padding-top:6px;padding-bottom:6px}.mat-list-item{font-family:Roboto,"Helvetica Neue",sans-serif}.mat-list .mat-list-item,.mat-nav-list .mat-list-item{font-size:16px}.mat-list .mat-list-item .mat-line,.mat-nav-list .mat-list-item .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-list .mat-list-item .mat-line:nth-child(n+2),.mat-nav-list .mat-list-item .mat-line:nth-child(n+2){font-size:14px}.mat-list .mat-subheader,.mat-nav-list .mat-subheader{font:500 14px/24px Roboto,"Helvetica Neue",sans-serif}.mat-list[dense] .mat-list-item,.mat-nav-list[dense] .mat-list-item{font-size:12px}.mat-list[dense] .mat-list-item .mat-line,.mat-nav-list[dense] .mat-list-item .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-list[dense] .mat-list-item .mat-line:nth-child(n+2),.mat-nav-list[dense] .mat-list-item .mat-line:nth-child(n+2){font-size:12px}.mat-list[dense] .mat-subheader,.mat-nav-list[dense] .mat-subheader{font:500 12px Roboto,"Helvetica Neue",sans-serif}.mat-option{font-family:Roboto,"Helvetica Neue",sans-serif;font-size:16px}.mat-simple-snackbar{font-family:Roboto,"Helvetica Neue",sans-serif;font-size:14px}.mat-simple-snackbar-action{line-height:1;font-family:inherit;font-size:inherit;font-weight:500}.mat-ripple{overflow:hidden}.mat-ripple.mat-ripple-unbounded{overflow:visible}.mat-ripple-element{position:absolute;border-radius:50%;pointer-events:none;transition:opacity,transform 0s cubic-bezier(0,0,.2,1);transform:scale(0)}.mat-option{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;line-height:48px;height:48px;padding:0 16px;text-align:left;text-decoration:none;position:relative;cursor:pointer;outline:0}.mat-option[disabled]{cursor:default}[dir=rtl] .mat-option{text-align:right}.mat-option .mat-icon{margin-right:16px}[dir=rtl] .mat-option .mat-icon{margin-left:16px;margin-right:0}.mat-option[aria-disabled=true]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.mat-optgroup .mat-option:not(.mat-option-multiple){padding-left:32px}[dir=rtl] .mat-optgroup .mat-option:not(.mat-option-multiple){padding-left:16px;padding-right:32px}.mat-option-ripple{position:absolute;top:0;left:0;bottom:0;right:0;pointer-events:none}@media screen and (-ms-high-contrast:active){.mat-option-ripple{opacity:.5}}.mat-option-pseudo-checkbox{margin-right:8px}[dir=rtl] .mat-option-pseudo-checkbox{margin-left:8px;margin-right:0}.mat-optgroup-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;line-height:48px;height:48px;padding:0 16px;text-align:left;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;font-weight:700;font-size:14px}.mat-optgroup-label[disabled]{cursor:default}[dir=rtl] .mat-optgroup-label{text-align:right}.mat-optgroup-label .mat-icon{margin-right:16px}[dir=rtl] .mat-optgroup-label .mat-icon{margin-left:16px;margin-right:0}.cdk-visually-hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;text-transform:none;width:1px}.cdk-global-overlay-wrapper,.cdk-overlay-container{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000}.cdk-overlay-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;z-index:1000;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48}.cdk-overlay-dark-backdrop{background:rgba(0,0,0,.6)}.cdk-overlay-transparent-backdrop{background:0 0}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll}.mat-ripple-element{background-color:rgba(0,0,0,.1)}.mat-option{color:rgba(0,0,0,.87)}.mat-option:focus:not(.mat-option-disabled),.mat-option:hover:not(.mat-option-disabled){background:rgba(0,0,0,.04)}.mat-option.mat-selected.mat-primary,.mat-primary .mat-option.mat-selected{color:#3f51b5}.mat-accent .mat-option.mat-selected,.mat-option.mat-selected.mat-accent{color:#ff4081}.mat-option.mat-selected.mat-warn,.mat-warn .mat-option.mat-selected{color:#f44336}.mat-option.mat-selected:not(.mat-option-multiple){background:rgba(0,0,0,.04)}.mat-option.mat-active{background:rgba(0,0,0,.04);color:rgba(0,0,0,.87)}.mat-option.mat-option-disabled{color:rgba(0,0,0,.38)}.mat-optgroup-label{color:rgba(0,0,0,.54)}.mat-optgroup-disabled .mat-optgroup-label{color:rgba(0,0,0,.38)}.mat-pseudo-checkbox{color:rgba(0,0,0,.54)}.mat-pseudo-checkbox::after{color:#fafafa}.mat-primary .mat-pseudo-checkbox-checked,.mat-primary .mat-pseudo-checkbox-indeterminate,.mat-pseudo-checkbox-checked.mat-primary,.mat-pseudo-checkbox-indeterminate.mat-primary{background:#3f51b5}.mat-accent .mat-pseudo-checkbox-checked,.mat-accent .mat-pseudo-checkbox-indeterminate,.mat-pseudo-checkbox-checked.mat-accent,.mat-pseudo-checkbox-indeterminate.mat-accent{background:#ff4081}.mat-pseudo-checkbox-checked.mat-warn,.mat-pseudo-checkbox-indeterminate.mat-warn,.mat-warn .mat-pseudo-checkbox-checked,.mat-warn .mat-pseudo-checkbox-indeterminate{background:#f44336}.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled,.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled{background:#b0b0b0}.mat-app-background{background-color:#fafafa}.mat-theme-loaded-marker{display:none}.mat-autocomplete-panel{background:#fff;color:rgba(0,0,0,.87)}.mat-autocomplete-panel .mat-option.mat-selected:not(.mat-active):not(:hover){background:#fff;color:rgba(0,0,0,.87)}.mat-button,.mat-icon-button{background:0 0}.mat-button.mat-primary .mat-button-focus-overlay,.mat-icon-button.mat-primary .mat-button-focus-overlay{background-color:rgba(63,81,181,.12)}.mat-button.mat-accent .mat-button-focus-overlay,.mat-icon-button.mat-accent .mat-button-focus-overlay{background-color:rgba(255,64,129,.12)}.mat-button.mat-warn .mat-button-focus-overlay,.mat-icon-button.mat-warn .mat-button-focus-overlay{background-color:rgba(244,67,54,.12)}.mat-button[disabled] .mat-button-focus-overlay,.mat-icon-button[disabled] .mat-button-focus-overlay{background-color:transparent}.mat-button.mat-primary,.mat-icon-button.mat-primary{color:#3f51b5}.mat-button.mat-accent,.mat-icon-button.mat-accent{color:#ff4081}.mat-button.mat-warn,.mat-icon-button.mat-warn{color:#f44336}.mat-button.mat-accent[disabled],.mat-button.mat-primary[disabled],.mat-button.mat-warn[disabled],.mat-button[disabled][disabled],.mat-icon-button.mat-accent[disabled],.mat-icon-button.mat-primary[disabled],.mat-icon-button.mat-warn[disabled],.mat-icon-button[disabled][disabled]{color:rgba(0,0,0,.38)}.mat-fab,.mat-mini-fab,.mat-raised-button{color:rgba(0,0,0,.87);background-color:#fff}.mat-fab.mat-primary,.mat-mini-fab.mat-primary,.mat-raised-button.mat-primary{color:rgba(255,255,255,.87)}.mat-fab.mat-accent,.mat-mini-fab.mat-accent,.mat-raised-button.mat-accent{color:#fff}.mat-fab.mat-warn,.mat-mini-fab.mat-warn,.mat-raised-button.mat-warn{color:#fff}.mat-fab.mat-accent[disabled],.mat-fab.mat-primary[disabled],.mat-fab.mat-warn[disabled],.mat-fab[disabled][disabled],.mat-mini-fab.mat-accent[disabled],.mat-mini-fab.mat-primary[disabled],.mat-mini-fab.mat-warn[disabled],.mat-mini-fab[disabled][disabled],.mat-raised-button.mat-accent[disabled],.mat-raised-button.mat-primary[disabled],.mat-raised-button.mat-warn[disabled],.mat-raised-button[disabled][disabled]{color:rgba(0,0,0,.38)}.mat-fab.mat-primary,.mat-mini-fab.mat-primary,.mat-raised-button.mat-primary{background-color:#3f51b5}.mat-fab.mat-accent,.mat-mini-fab.mat-accent,.mat-raised-button.mat-accent{background-color:#ff4081}.mat-fab.mat-warn,.mat-mini-fab.mat-warn,.mat-raised-button.mat-warn{background-color:#f44336}.mat-fab.mat-accent[disabled],.mat-fab.mat-primary[disabled],.mat-fab.mat-warn[disabled],.mat-fab[disabled][disabled],.mat-mini-fab.mat-accent[disabled],.mat-mini-fab.mat-primary[disabled],.mat-mini-fab.mat-warn[disabled],.mat-mini-fab[disabled][disabled],.mat-raised-button.mat-accent[disabled],.mat-raised-button.mat-primary[disabled],.mat-raised-button.mat-warn[disabled],.mat-raised-button[disabled][disabled]{background-color:rgba(0,0,0,.12)}.mat-fab.mat-primary .mat-ripple-element,.mat-mini-fab.mat-primary .mat-ripple-element,.mat-raised-button.mat-primary .mat-ripple-element{background-color:rgba(255,255,255,.2)}.mat-fab.mat-accent .mat-ripple-element,.mat-mini-fab.mat-accent .mat-ripple-element,.mat-raised-button.mat-accent .mat-ripple-element{background-color:rgba(255,255,255,.2)}.mat-fab.mat-warn .mat-ripple-element,.mat-mini-fab.mat-warn .mat-ripple-element,.mat-raised-button.mat-warn .mat-ripple-element{background-color:rgba(255,255,255,.2)}.mat-button.mat-primary .mat-ripple-element{background-color:rgba(63,81,181,.1)}.mat-button.mat-accent .mat-ripple-element{background-color:rgba(255,64,129,.1)}.mat-button.mat-warn .mat-ripple-element{background-color:rgba(244,67,54,.1)}.mat-icon-button.mat-primary .mat-ripple-element{background-color:rgba(63,81,181,.2)}.mat-icon-button.mat-accent .mat-ripple-element{background-color:rgba(255,64,129,.2)}.mat-icon-button.mat-warn .mat-ripple-element{background-color:rgba(244,67,54,.2)}.mat-button-toggle{color:rgba(0,0,0,.38)}.mat-button-toggle.cdk-focused .mat-button-toggle-focus-overlay{background-color:rgba(0,0,0,.06)}.mat-button-toggle-checked{background-color:#e0e0e0;color:#000}.mat-button-toggle-disabled{background-color:#eee;color:rgba(0,0,0,.38)}.mat-button-toggle-disabled.mat-button-toggle-checked{background-color:#bdbdbd}.mat-card{background:#fff;color:rgba(0,0,0,.87)}.mat-card-subtitle{color:rgba(0,0,0,.54)}.mat-checkbox-frame{border-color:rgba(0,0,0,.54)}.mat-checkbox-checkmark{fill:#fafafa}.mat-checkbox-checkmark-path{stroke:#fafafa!important}.mat-checkbox-mixedmark{background-color:#fafafa}.mat-checkbox-checked.mat-primary .mat-checkbox-background,.mat-checkbox-indeterminate.mat-primary .mat-checkbox-background{background-color:#3f51b5}.mat-checkbox-checked.mat-accent .mat-checkbox-background,.mat-checkbox-indeterminate.mat-accent .mat-checkbox-background{background-color:#ff4081}.mat-checkbox-checked.mat-warn .mat-checkbox-background,.mat-checkbox-indeterminate.mat-warn .mat-checkbox-background{background-color:#f44336}.mat-checkbox-disabled.mat-checkbox-checked .mat-checkbox-background,.mat-checkbox-disabled.mat-checkbox-indeterminate .mat-checkbox-background{background-color:#b0b0b0}.mat-checkbox-disabled:not(.mat-checkbox-checked) .mat-checkbox-frame{border-color:#b0b0b0}.mat-checkbox-disabled .mat-checkbox-label{color:#b0b0b0}.mat-checkbox:not(.mat-checkbox-disabled).mat-primary .mat-checkbox-ripple .mat-ripple-element{background-color:rgba(63,81,181,.26)}.mat-checkbox:not(.mat-checkbox-disabled).mat-accent .mat-checkbox-ripple .mat-ripple-element{background-color:rgba(255,64,129,.26)}.mat-checkbox:not(.mat-checkbox-disabled).mat-warn .mat-checkbox-ripple .mat-ripple-element{background-color:rgba(244,67,54,.26)}.mat-chip:not(.mat-basic-chip){background-color:#e0e0e0;color:rgba(0,0,0,.87)}.mat-chip.mat-chip-selected:not(.mat-basic-chip){background-color:grey;color:rgba(255,255,255,.87)}.mat-chip.mat-chip-selected:not(.mat-basic-chip).mat-primary{background-color:#3f51b5;color:rgba(255,255,255,.87)}.mat-chip.mat-chip-selected:not(.mat-basic-chip).mat-accent{background-color:#ff4081;color:#fff}.mat-chip.mat-chip-selected:not(.mat-basic-chip).mat-warn{background-color:#f44336;color:#fff}.mat-datepicker-content{background-color:#fff}.mat-calendar-arrow{border-top-color:rgba(0,0,0,.54)}.mat-calendar-next-button,.mat-calendar-previous-button{color:rgba(0,0,0,.54)}.mat-calendar-table-header{color:rgba(0,0,0,.38)}.mat-calendar-table-header-divider::after{background:rgba(0,0,0,.12)}.mat-calendar-body-label{color:rgba(0,0,0,.54)}.mat-calendar-body-cell-content{color:rgba(0,0,0,.87);border-color:transparent}.mat-calendar-body-disabled>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected){color:rgba(0,0,0,.38)}.cdk-keyboard-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected),:not(.mat-calendar-body-disabled):hover>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected){background-color:rgba(0,0,0,.04)}.mat-calendar-body-selected{background-color:#3f51b5;color:rgba(255,255,255,.87)}.mat-calendar-body-disabled>.mat-calendar-body-selected{background-color:rgba(63,81,181,.4)}.mat-calendar-body-today:not(.mat-calendar-body-selected){border-color:rgba(0,0,0,.38)}.mat-calendar-body-today.mat-calendar-body-selected{box-shadow:inset 0 0 0 1px rgba(255,255,255,.87)}.mat-calendar-body-disabled>.mat-calendar-body-today:not(.mat-calendar-body-selected){border-color:rgba(0,0,0,.18)}.mat-dialog-container{background:#fff;color:rgba(0,0,0,.87)}.mat-expansion-panel{background:#fff;color:#000}.mat-action-row{border-top-color:rgba(0,0,0,.12)}.mat-expansion-panel-header:focus,.mat-expansion-panel-header:hover{background:rgba(0,0,0,.04)}.mat-expansion-panel-header-title{color:rgba(0,0,0,.87)}.mat-expansion-panel-header-description{color:rgba(0,0,0,.54)}.mat-expansion-indicator::after{color:rgba(0,0,0,.54)}.mat-icon.mat-primary{color:#3f51b5}.mat-icon.mat-accent{color:#ff4081}.mat-icon.mat-warn{color:#f44336}.mat-input-placeholder{color:rgba(0,0,0,.38)}.mat-focused .mat-input-placeholder{color:#3f51b5}.mat-focused .mat-input-placeholder.mat-accent{color:#ff4081}.mat-focused .mat-input-placeholder.mat-warn{color:#f44336}.mat-input-element:disabled{color:rgba(0,0,0,.38)}.mat-focused .mat-input-placeholder.mat-float .mat-placeholder-required,input.mat-input-element:-webkit-autofill+.mat-input-placeholder .mat-placeholder-required{color:#ff4081}.mat-input-underline{background-color:rgba(0,0,0,.12)}.mat-input-ripple{background-color:#3f51b5}.mat-input-ripple.mat-accent{background-color:#ff4081}.mat-input-ripple.mat-warn{background-color:#f44336}.mat-input-invalid .mat-input-placeholder{color:#f44336}.mat-input-invalid .mat-input-placeholder.mat-accent,.mat-input-invalid .mat-input-placeholder.mat-float .mat-placeholder-required{color:#f44336}.mat-input-invalid .mat-input-ripple{background-color:#f44336}.mat-input-error{color:#f44336}.mat-list .mat-list-item,.mat-nav-list .mat-list-item{color:rgba(0,0,0,.87)}.mat-list .mat-subheader,.mat-nav-list .mat-subheader{color:rgba(0,0,0,.54)}.mat-divider{border-top-color:rgba(0,0,0,.12)}.mat-nav-list .mat-list-item{outline:0}.mat-nav-list .mat-list-item.mat-list-item-focus,.mat-nav-list .mat-list-item:hover{background:rgba(0,0,0,.04)}.mat-menu-content{background:#fff}.mat-menu-item{background:0 0;color:rgba(0,0,0,.87)}.mat-menu-item[disabled]{color:rgba(0,0,0,.38)}.mat-menu-item .mat-icon{color:rgba(0,0,0,.54);vertical-align:middle}.mat-menu-item:focus:not([disabled]),.mat-menu-item:hover:not([disabled]){background:rgba(0,0,0,.04)}.mat-progress-bar-background{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%271.1%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20xmlns%3Axlink%3D%27http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%27%20x%3D%270px%27%20y%3D%270px%27%20enable-background%3D%27new%200%200%205%202%27%20xml%3Aspace%3D%27preserve%27%20viewBox%3D%270%200%205%202%27%20preserveAspectRatio%3D%27none%20slice%27%3E%3Ccircle%20cx%3D%271%27%20cy%3D%271%27%20r%3D%271%27%20fill%3D%27%23c5cae9%27%2F%3E%3C%2Fsvg%3E")}.mat-progress-bar-buffer{background-color:#c5cae9}.mat-progress-bar-fill::after{background-color:#3f51b5}.mat-progress-bar.mat-accent .mat-progress-bar-background{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%271.1%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20xmlns%3Axlink%3D%27http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%27%20x%3D%270px%27%20y%3D%270px%27%20enable-background%3D%27new%200%200%205%202%27%20xml%3Aspace%3D%27preserve%27%20viewBox%3D%270%200%205%202%27%20preserveAspectRatio%3D%27none%20slice%27%3E%3Ccircle%20cx%3D%271%27%20cy%3D%271%27%20r%3D%271%27%20fill%3D%27%23ff80ab%27%2F%3E%3C%2Fsvg%3E")}.mat-progress-bar.mat-accent .mat-progress-bar-buffer{background-color:#ff80ab}.mat-progress-bar.mat-accent .mat-progress-bar-fill::after{background-color:#ff4081}.mat-progress-bar.mat-warn .mat-progress-bar-background{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%271.1%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20xmlns%3Axlink%3D%27http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%27%20x%3D%270px%27%20y%3D%270px%27%20enable-background%3D%27new%200%200%205%202%27%20xml%3Aspace%3D%27preserve%27%20viewBox%3D%270%200%205%202%27%20preserveAspectRatio%3D%27none%20slice%27%3E%3Ccircle%20cx%3D%271%27%20cy%3D%271%27%20r%3D%271%27%20fill%3D%27%23ffcdd2%27%2F%3E%3C%2Fsvg%3E")}.mat-progress-bar.mat-warn .mat-progress-bar-buffer{background-color:#ffcdd2}.mat-progress-bar.mat-warn .mat-progress-bar-fill::after{background-color:#f44336}.mat-progress-spinner path,.mat-spinner path{stroke:#3f51b5}.mat-progress-spinner.mat-accent path,.mat-spinner.mat-accent path{stroke:#ff4081}.mat-progress-spinner.mat-warn path,.mat-spinner.mat-warn path{stroke:#f44336}.mat-radio-outer-circle{border-color:rgba(0,0,0,.54)}.mat-radio-disabled .mat-radio-outer-circle{border-color:rgba(0,0,0,.38)}.mat-radio-disabled .mat-radio-inner-circle,.mat-radio-disabled .mat-radio-ripple .mat-ripple-element{background-color:rgba(0,0,0,.38)}.mat-radio-disabled .mat-radio-label-content{color:rgba(0,0,0,.38)}.mat-radio-button.mat-primary.mat-radio-checked .mat-radio-outer-circle{border-color:#3f51b5}.mat-radio-button.mat-primary .mat-radio-inner-circle{background-color:#3f51b5}.mat-radio-button.mat-primary .mat-radio-ripple .mat-ripple-element{background-color:rgba(63,81,181,.26)}.mat-radio-button.mat-accent.mat-radio-checked .mat-radio-outer-circle{border-color:#ff4081}.mat-radio-button.mat-accent .mat-radio-inner-circle{background-color:#ff4081}.mat-radio-button.mat-accent .mat-radio-ripple .mat-ripple-element{background-color:rgba(255,64,129,.26)}.mat-radio-button.mat-warn.mat-radio-checked .mat-radio-outer-circle{border-color:#f44336}.mat-radio-button.mat-warn .mat-radio-inner-circle{background-color:#f44336}.mat-radio-button.mat-warn .mat-radio-ripple .mat-ripple-element{background-color:rgba(244,67,54,.26)}.mat-select-arrow,.mat-select-trigger{color:rgba(0,0,0,.38)}.mat-select-underline{background-color:rgba(0,0,0,.12)}.mat-select-arrow,.mat-select-disabled .mat-select-value,.mat-select-trigger{color:rgba(0,0,0,.38)}.mat-select-content,.mat-select-panel-done-animating{background:#fff}.mat-select-value{color:rgba(0,0,0,.87)}.mat-select:focus:not(.mat-select-disabled).mat-primary .mat-select-arrow,.mat-select:focus:not(.mat-select-disabled).mat-primary .mat-select-trigger{color:#3f51b5}.mat-select:focus:not(.mat-select-disabled).mat-primary .mat-select-underline{background-color:#3f51b5}.mat-select:focus:not(.mat-select-disabled).mat-accent .mat-select-arrow,.mat-select:focus:not(.mat-select-disabled).mat-accent .mat-select-trigger{color:#ff4081}.mat-select:focus:not(.mat-select-disabled).mat-accent .mat-select-underline{background-color:#ff4081}.mat-select:focus:not(.mat-select-disabled).mat-warn .mat-select-arrow,.mat-select:focus:not(.mat-select-disabled).mat-warn .mat-select-trigger,.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) .mat-select-arrow,.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) .mat-select-trigger{color:#f44336}.mat-select:focus:not(.mat-select-disabled).mat-warn .mat-select-underline,.mat-select:not(:focus).ng-invalid.ng-touched:not(.mat-select-disabled) .mat-select-underline{background-color:#f44336}.mat-sidenav-container{background-color:#fafafa;color:rgba(0,0,0,.87)}.mat-sidenav{background-color:#fff;color:rgba(0,0,0,.87)}.mat-sidenav.mat-sidenav-push{background-color:#fff}.mat-sidenav-backdrop.mat-sidenav-shown{background-color:rgba(0,0,0,.6)}.mat-slide-toggle.mat-checked:not(.mat-disabled) .mat-slide-toggle-thumb{background-color:#e91e63}.mat-slide-toggle.mat-checked:not(.mat-disabled) .mat-slide-toggle-bar{background-color:rgba(233,30,99,.5)}.mat-slide-toggle:not(.mat-checked) .mat-ripple-element{background-color:rgba(0,0,0,.06)}.mat-slide-toggle .mat-ripple-element{background-color:rgba(233,30,99,.12)}.mat-slide-toggle.mat-primary.mat-checked:not(.mat-disabled) .mat-slide-toggle-thumb{background-color:#3f51b5}.mat-slide-toggle.mat-primary.mat-checked:not(.mat-disabled) .mat-slide-toggle-bar{background-color:rgba(63,81,181,.5)}.mat-slide-toggle.mat-primary:not(.mat-checked) .mat-ripple-element{background-color:rgba(0,0,0,.06)}.mat-slide-toggle.mat-primary .mat-ripple-element{background-color:rgba(63,81,181,.12)}.mat-slide-toggle.mat-warn.mat-checked:not(.mat-disabled) .mat-slide-toggle-thumb{background-color:#f44336}.mat-slide-toggle.mat-warn.mat-checked:not(.mat-disabled) .mat-slide-toggle-bar{background-color:rgba(244,67,54,.5)}.mat-slide-toggle.mat-warn:not(.mat-checked) .mat-ripple-element{background-color:rgba(0,0,0,.06)}.mat-slide-toggle.mat-warn .mat-ripple-element{background-color:rgba(244,67,54,.12)}.mat-disabled .mat-slide-toggle-thumb{background-color:#bdbdbd}.mat-disabled .mat-slide-toggle-bar{background-color:rgba(0,0,0,.1)}.mat-slide-toggle-thumb{background-color:#fafafa}.mat-slide-toggle-bar{background-color:rgba(0,0,0,.38)}.mat-slider-track-background{background-color:rgba(0,0,0,.26)}.mat-primary .mat-slider-thumb,.mat-primary .mat-slider-thumb-label,.mat-primary .mat-slider-track-fill{background-color:#3f51b5}.mat-primary .mat-slider-thumb-label-text{color:rgba(255,255,255,.87)}.mat-accent .mat-slider-thumb,.mat-accent .mat-slider-thumb-label,.mat-accent .mat-slider-track-fill{background-color:#ff4081}.mat-accent .mat-slider-thumb-label-text{color:#fff}.mat-warn .mat-slider-thumb,.mat-warn .mat-slider-thumb-label,.mat-warn .mat-slider-track-fill{background-color:#f44336}.mat-warn .mat-slider-thumb-label-text{color:#fff}.mat-slider-focus-ring{background-color:rgba(255,64,129,.2)}.cdk-focused .mat-slider-track-background,.mat-slider:hover .mat-slider-track-background{background-color:rgba(0,0,0,.38)}.mat-slider-disabled .mat-slider-thumb,.mat-slider-disabled .mat-slider-track-background,.mat-slider-disabled .mat-slider-track-fill{background-color:rgba(0,0,0,.26)}.mat-slider-disabled:hover .mat-slider-track-background{background-color:rgba(0,0,0,.26)}.mat-slider-min-value .mat-slider-focus-ring{background-color:rgba(0,0,0,.12)}.mat-slider-min-value.mat-slider-thumb-label-showing .mat-slider-thumb,.mat-slider-min-value.mat-slider-thumb-label-showing .mat-slider-thumb-label{background-color:#000}.mat-slider-min-value.mat-slider-thumb-label-showing.cdk-focused .mat-slider-thumb,.mat-slider-min-value.mat-slider-thumb-label-showing.cdk-focused .mat-slider-thumb-label{background-color:rgba(0,0,0,.26)}.mat-slider-min-value:not(.mat-slider-thumb-label-showing) .mat-slider-thumb{border-color:rgba(0,0,0,.26);background-color:transparent}.mat-slider-min-value:not(.mat-slider-thumb-label-showing).cdk-focused .mat-slider-thumb,.mat-slider-min-value:not(.mat-slider-thumb-label-showing):hover .mat-slider-thumb{border-color:rgba(0,0,0,.38)}.mat-slider-min-value:not(.mat-slider-thumb-label-showing).cdk-focused.mat-slider-disabled .mat-slider-thumb,.mat-slider-min-value:not(.mat-slider-thumb-label-showing):hover.mat-slider-disabled .mat-slider-thumb{border-color:rgba(0,0,0,.26)}.mat-slider-has-ticks .mat-slider-wrapper::after{border-color:rgba(0,0,0,.7)}.mat-slider-horizontal .mat-slider-ticks{background-image:repeating-linear-gradient(to right,rgba(0,0,0,.7),rgba(0,0,0,.7) 2px,transparent 0,transparent);background-image:-moz-repeating-linear-gradient(.0001deg,rgba(0,0,0,.7),rgba(0,0,0,.7) 2px,transparent 0,transparent)}.mat-slider-vertical .mat-slider-ticks{background-image:repeating-linear-gradient(to bottom,rgba(0,0,0,.7),rgba(0,0,0,.7) 2px,transparent 0,transparent)}.mat-tab-header,.mat-tab-nav-bar{border-bottom:1px solid rgba(0,0,0,.12)}.mat-tab-group-inverted-header .mat-tab-header,.mat-tab-group-inverted-header .mat-tab-nav-bar{border-top:1px solid rgba(0,0,0,.12);border-bottom:none}.mat-tab-label:focus{background-color:rgba(197,202,233,.3)}.mat-ink-bar{background-color:#3f51b5}.mat-tab-label,.mat-tab-link{color:rgba(0,0,0,.87)}.mat-tab-label.mat-tab-disabled,.mat-tab-link.mat-tab-disabled{color:rgba(0,0,0,.38)}.mat-toolbar{background:#f5f5f5;color:rgba(0,0,0,.87)}.mat-toolbar.mat-primary{background:#3f51b5;color:rgba(255,255,255,.87)}.mat-toolbar.mat-accent{background:#ff4081;color:#fff}.mat-toolbar.mat-warn{background:#f44336;color:#fff}.mat-tooltip{background:rgba(97,97,97,.9)} -------------------------------------------------------------------------------- /clientapp/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /clientapp/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 | -------------------------------------------------------------------------------- /clientapp/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsavkin/state_management_ngrx4/639114adcdbbdc57ee4f747792b22d8f48e03dad/clientapp/src/favicon.ico -------------------------------------------------------------------------------- /clientapp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Clientapp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /clientapp/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 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /clientapp/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 to support `@angular/animation`. */ 41 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | import 'core-js/es6/reflect'; 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 50 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 51 | 52 | 53 | 54 | /*************************************************************************************************** 55 | * Zone JS is required by Angular itself. 56 | */ 57 | import 'zone.js/dist/zone'; // Included with Angular CLI. 58 | 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | 65 | /** 66 | * Date, currency, decimal and percent pipes. 67 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 68 | */ 69 | // import 'intl'; // Run `npm install --save intl`. 70 | /** 71 | * Need to import at least one locale-data with intl. 72 | */ 73 | // import 'intl/locale-data/jsonp/en'; 74 | -------------------------------------------------------------------------------- /clientapp/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /clientapp/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 | -------------------------------------------------------------------------------- /clientapp/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /clientapp/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /clientapp/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /clientapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /clientapp/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": false, 14 | "forin": true, 15 | "import-blacklist": [ 16 | true, 17 | "rxjs" 18 | ], 19 | "import-spacing": true, 20 | "indent": [ 21 | true, 22 | "spaces" 23 | ], 24 | "label-position": true, 25 | "max-line-length": [ 26 | true, 27 | 140 28 | ], 29 | "member-access": false, 30 | "member-ordering": [ 31 | true, 32 | "static-before-instance", 33 | "variables-before-functions" 34 | ], 35 | "no-arg": true, 36 | "no-bitwise": true, 37 | "no-console": [ 38 | true, 39 | "debug", 40 | "info", 41 | "time", 42 | "timeEnd", 43 | "trace" 44 | ], 45 | "no-construct": true, 46 | "no-debugger": true, 47 | "no-duplicate-super": true, 48 | "no-empty": false, 49 | "no-empty-interface": true, 50 | "no-eval": true, 51 | "no-inferrable-types": [ 52 | true, 53 | "ignore-params" 54 | ], 55 | "no-misused-new": true, 56 | "no-non-null-assertion": true, 57 | "no-shadowed-variable": true, 58 | "no-string-literal": false, 59 | "no-string-throw": true, 60 | "no-switch-case-fall-through": true, 61 | "no-trailing-whitespace": true, 62 | "no-unnecessary-initializer": true, 63 | "no-unused-expression": true, 64 | "no-use-before-declare": true, 65 | "no-var-keyword": true, 66 | "object-literal-sort-keys": false, 67 | "one-line": [ 68 | true, 69 | "check-open-brace", 70 | "check-catch", 71 | "check-else", 72 | "check-whitespace" 73 | ], 74 | "prefer-const": true, 75 | "radix": true, 76 | "semicolon": [ 77 | "always" 78 | ], 79 | "triple-equals": [ 80 | true, 81 | "allow-null-check" 82 | ], 83 | "typedef-whitespace": [ 84 | true, 85 | { 86 | "call-signature": "nospace", 87 | "index-signature": "nospace", 88 | "parameter": "nospace", 89 | "property-declaration": "nospace", 90 | "variable-declaration": "nospace" 91 | } 92 | ], 93 | "typeof-compare": true, 94 | "unified-signatures": true, 95 | "variable-name": false, 96 | "whitespace": [ 97 | true, 98 | "check-branch", 99 | "check-decl", 100 | "check-operator", 101 | "check-separator", 102 | "check-type" 103 | ], 104 | "use-input-property-decorator": true, 105 | "use-output-property-decorator": true, 106 | "use-host-property-decorator": true, 107 | "no-input-rename": true, 108 | "no-output-rename": true, 109 | "use-life-cycle-interface": true, 110 | "use-pipe-transform-interface": true, 111 | "component-class-suffix": true, 112 | "directive-class-suffix": true, 113 | "no-access-missing-member": true, 114 | "templates-use-public": true, 115 | "invoke-injectable": true 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-app", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "server": "tsc && node dist/server.js" 8 | }, 9 | "dependencies": { 10 | "express": "4.14.0", 11 | "body-parser": "1.16.0" 12 | }, 13 | "devDependencies": { 14 | "typescript": "2.1.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as express from "express"; 3 | import * as bodyParser from "body-parser"; 4 | 5 | const app = express(); 6 | 7 | const port = 4444; 8 | 9 | const router = express.Router(); 10 | 11 | const _talks = [ 12 | { 13 | "id": 898, 14 | "title": "Are we there yet?", 15 | "speaker": "Rich Hickey", 16 | "description": "In his keynote at JVM Languages Summit 2009, Rich Hickey advocated for the reexamination of basic principles like state, identity, value, time, types, genericity, complexity, as they are used by OOP today, to be able to create the new constructs and languages to deal with the massive parallelism and concurrency of the future.", 17 | "yourRating": null, 18 | "rating": 9.1 19 | }, 20 | { 21 | "id": 777, 22 | "title": "The Value of Values", 23 | "speaker": "Rich Hickey", 24 | "description": "Rich Hickey compares value-oriented programming with place-oriented programming concluding that the time of imperative languages has passed and it is the time of functional programming.", 25 | "yourRating": null, 26 | "rating": 8.5 27 | }, 28 | { 29 | "id": 466, 30 | "title": "Simple Made Easy", 31 | "speaker": "Rich Hickey", 32 | "description": "Rich Hickey emphasizes simplicity’s virtues over easiness’, showing that while many choose easiness they may end up with complexity, and the better way is to choose easiness along the simplicity path.", 33 | "yourRating": null, 34 | "rating": 8.2 35 | }, 36 | { 37 | "id": 322, 38 | "title": "Growing a Language", 39 | "speaker": "Guy Steele", 40 | "description": "Guy Steele's keynote at the 1998 ACM OOPSLA conference on 'Growing a Language' discusses the importance of and issues associated with designing a programming language that can be grown by its users.", 41 | "yourRating": null, 42 | "rating": 8.9 43 | } 44 | ]; 45 | 46 | 47 | router.get("/talks", (req, res) => { 48 | const filters = req.query; 49 | console.log("GET /talks", "filters:", filters); 50 | const filteredTalks = _talks.filter(t => { 51 | const titlePass = filters.title ? t.title.indexOf(filters.title) > -1 : true; 52 | const speakerPass = filters.speaker ? t.speaker.indexOf(filters.speaker) > -1 : true; 53 | const ratingPass = filters.minRating ? t.rating >= filters.minRating : true; 54 | return titlePass && speakerPass && ratingPass; 55 | }); 56 | 57 | const talks = filteredTalks.reduce((acc, t) => (acc[t.id] = t, acc), {}); 58 | const list = filteredTalks.map(t => t.id); 59 | 60 | res.json({talks, list}); 61 | }); 62 | 63 | router.get("/talk", (req, res) => { 64 | const id = +req.query.id; 65 | console.log("GET /talk", "id:", id); 66 | const talk = _talks.filter(t => t.id === id)[0]; 67 | res.json({talk}); 68 | }); 69 | 70 | router.post("/rate", (req, res) => { 71 | const id = req.body.id; 72 | const yourRating = req.body.yourRating; 73 | console.log("POST /rate", "id:", id, "yourRating:", yourRating); 74 | 75 | if (yourRating > 10) { 76 | res.status(500); 77 | res.json({status: 'ERROR', message: "Rating cannot be > 10"}); 78 | } else { 79 | const talk = _talks.filter(t => t.id === id)[0]; 80 | talk.yourRating = yourRating; 81 | res.json({status: 'OK'}); 82 | } 83 | }); 84 | 85 | app.use(function(req, res, next) { 86 | res.header("Access-Control-Allow-Origin", "*"); 87 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 88 | next(); 89 | }); 90 | 91 | app.use(bodyParser.json()); 92 | app.use(bodyParser.urlencoded({ extended: true })); 93 | app.use("/", router); 94 | app.listen(port); 95 | 96 | console.log(`Server port: ${port}`); -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "mapRoot": "./", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "outDir": "./dist", 11 | "sourceMap": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "./node_modules/@types" 15 | ] 16 | }, 17 | "files": [ 18 | "./server.ts" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------