├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── angular.json ├── apps ├── .gitkeep ├── dashboard-e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json └── dashboard │ ├── browserslist │ ├── karma.conf.js │ ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── projects │ │ │ ├── project-details │ │ │ ├── project-details.component.html │ │ │ ├── project-details.component.scss │ │ │ ├── project-details.component.spec.ts │ │ │ └── project-details.component.ts │ │ │ ├── projects-list │ │ │ ├── projects-list.animations.ts │ │ │ ├── projects-list.component.html │ │ │ ├── projects-list.component.scss │ │ │ ├── projects-list.component.spec.ts │ │ │ └── projects-list.component.ts │ │ │ ├── projects-routing.module.ts │ │ │ ├── projects.component.html │ │ │ ├── projects.component.scss │ │ │ ├── projects.component.spec.ts │ │ │ ├── projects.component.ts │ │ │ ├── projects.module.spec.ts │ │ │ └── projects.module.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── background.jpg │ │ └── logo.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── karma.conf.js ├── libs ├── .gitkeep ├── core-data │ ├── karma.conf.js │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── auth │ │ │ │ ├── auth-guard.service.spec.ts │ │ │ │ ├── auth-guard.service.ts │ │ │ │ ├── auth.service.spec.ts │ │ │ │ ├── auth.service.ts │ │ │ │ ├── token-interceptor.service.spec.ts │ │ │ │ └── token.interceptor.ts │ │ │ ├── core-data.module.spec.ts │ │ │ ├── core-data.module.ts │ │ │ ├── customers │ │ │ │ ├── customer.model.ts │ │ │ │ ├── customers.service.spec.ts │ │ │ │ └── customers.service.ts │ │ │ ├── error │ │ │ │ ├── error-interceptor.service.spec.ts │ │ │ │ └── error.interceptor.ts │ │ │ ├── notifications │ │ │ │ └── notifications.service.ts │ │ │ ├── projects │ │ │ │ ├── project.model.ts │ │ │ │ ├── projects.service.spec.ts │ │ │ │ └── projects.service.ts │ │ │ └── state │ │ │ │ ├── customers │ │ │ │ ├── customers.actions.ts │ │ │ │ ├── customers.effects.ts │ │ │ │ ├── customers.facade.ts │ │ │ │ └── customers.reducer.ts │ │ │ │ ├── index.ts │ │ │ │ ├── projects │ │ │ │ ├── projects.actions.ts │ │ │ │ ├── projects.effects.ts │ │ │ │ ├── projects.facade.ts │ │ │ │ └── projects.reducer.ts │ │ │ │ ├── state.module.spec.ts │ │ │ │ └── state.module.ts │ │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── material │ ├── karma.conf.js │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── material.module.spec.ts │ │ │ └── material.module.ts │ │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── ui-login │ ├── karma.conf.js │ ├── src │ │ ├── assets │ │ │ └── background.jpg │ │ ├── index.ts │ │ ├── lib │ │ │ ├── login │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.scss │ │ │ │ ├── login.component.spec.ts │ │ │ │ └── login.component.ts │ │ │ ├── ui-login.module.spec.ts │ │ │ └── ui-login.module.ts │ │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json └── ui-toolbar │ ├── karma.conf.js │ ├── src │ ├── index.ts │ ├── lib │ │ ├── toolbar │ │ │ ├── toolbar.component.html │ │ │ ├── toolbar.component.scss │ │ │ ├── toolbar.component.spec.ts │ │ │ └── toolbar.component.ts │ │ ├── ui-toolbar.module.spec.ts │ │ └── ui-toolbar.module.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── nx.json ├── package-lock.json ├── package.json ├── server ├── db.json ├── server.js └── users.json ├── tools ├── schematics │ └── .gitkeep └── tsconfig.tools.json ├── 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 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Reactive Workshop 2 | 3 | For this workshop, we are going to build Redux state around the `projects` feature with NGRX. 4 | 5 | ## The Stack 6 | 7 | ### NRWL Workspace 8 | A NRWL workspace contains one or all of you Angular projects and libraries. It creates a monorepo for your applications domains. Nx helps add extra layer of tooling that can help manage your enterprise applications. 9 | 10 | ### Angular Material 11 | Angular Material is a UI library for Angular that gives you access to a modern material UI that works across web, mobile, and desktop applications with minimal custom CSS and setup. 12 | 13 | ### JSON Server 14 | Creates a quick and simple way to mock out a backend REST service. We can then deliver some mocked out data in JSON format to make sure everything is working as expected once our real backend is connected. 15 | 16 | ## Getting Started 17 | 18 | ``` 19 | npm install 20 | npm start 21 | ``` 22 | 23 | This will both boot up a data server on localhost:3000 and serve the frontend on localhost:4200. 24 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "", 5 | "projects": { 6 | "core-data": { 7 | "root": "libs/core-data", 8 | "sourceRoot": "libs/core-data/src", 9 | "projectType": "library", 10 | "prefix": "core", 11 | "architect": { 12 | "test": { 13 | "builder": "@angular-devkit/build-angular:karma", 14 | "options": { 15 | "main": "libs/core-data/src/test.ts", 16 | "tsConfig": "libs/core-data/tsconfig.spec.json", 17 | "karmaConfig": "libs/core-data/karma.conf.js" 18 | } 19 | }, 20 | "lint": { 21 | "builder": "@angular-devkit/build-angular:tslint", 22 | "options": { 23 | "tsConfig": [ 24 | "libs/core-data/tsconfig.lib.json", 25 | "libs/core-data/tsconfig.spec.json" 26 | ], 27 | "exclude": [ 28 | "**/node_modules/**" 29 | ] 30 | } 31 | } 32 | } 33 | }, 34 | "dashboard": { 35 | "root": "apps/dashboard/", 36 | "sourceRoot": "apps/dashboard/src", 37 | "projectType": "application", 38 | "prefix": "app", 39 | "schematics": {}, 40 | "architect": { 41 | "build": { 42 | "builder": "@angular-devkit/build-angular:browser", 43 | "options": { 44 | "outputPath": "dist/apps/dashboard", 45 | "index": "apps/dashboard/src/index.html", 46 | "main": "apps/dashboard/src/main.ts", 47 | "polyfills": "apps/dashboard/src/polyfills.ts", 48 | "tsConfig": "apps/dashboard/tsconfig.app.json", 49 | "assets": [ 50 | "apps/dashboard/src/favicon.ico", 51 | "apps/dashboard/src/assets" 52 | ], 53 | "styles": [ 54 | "apps/dashboard/src/styles.scss" 55 | ], 56 | "scripts": [] 57 | }, 58 | "configurations": { 59 | "production": { 60 | "fileReplacements": [ 61 | { 62 | "replace": "apps/dashboard/src/environments/environment.ts", 63 | "with": "apps/dashboard/src/environments/environment.prod.ts" 64 | } 65 | ], 66 | "optimization": true, 67 | "outputHashing": "all", 68 | "sourceMap": false, 69 | "extractCss": true, 70 | "namedChunks": false, 71 | "aot": true, 72 | "extractLicenses": true, 73 | "vendorChunk": false, 74 | "buildOptimizer": true 75 | } 76 | } 77 | }, 78 | "serve": { 79 | "builder": "@angular-devkit/build-angular:dev-server", 80 | "options": { 81 | "browserTarget": "dashboard:build" 82 | }, 83 | "configurations": { 84 | "production": { 85 | "browserTarget": "dashboard:build:production" 86 | } 87 | } 88 | }, 89 | "extract-i18n": { 90 | "builder": "@angular-devkit/build-angular:extract-i18n", 91 | "options": { 92 | "browserTarget": "dashboard:build" 93 | } 94 | }, 95 | "test": { 96 | "builder": "@angular-devkit/build-angular:karma", 97 | "options": { 98 | "main": "apps/dashboard/src/test.ts", 99 | "polyfills": "apps/dashboard/src/polyfills.ts", 100 | "tsConfig": "apps/dashboard/tsconfig.spec.json", 101 | "karmaConfig": "apps/dashboard/karma.conf.js", 102 | "styles": [ 103 | "apps/dashboard/src/styles.scss" 104 | ], 105 | "scripts": [], 106 | "assets": [ 107 | "apps/dashboard/src/favicon.ico", 108 | "apps/dashboard/src/assets" 109 | ] 110 | } 111 | }, 112 | "lint": { 113 | "builder": "@angular-devkit/build-angular:tslint", 114 | "options": { 115 | "tsConfig": [ 116 | "apps/dashboard/tsconfig.app.json", 117 | "apps/dashboard/tsconfig.spec.json" 118 | ], 119 | "exclude": [ 120 | "**/node_modules/**" 121 | ] 122 | } 123 | } 124 | } 125 | }, 126 | "dashboard-e2e": { 127 | "root": "apps/dashboard-e2e/", 128 | "projectType": "application", 129 | "architect": { 130 | "e2e": { 131 | "builder": "@angular-devkit/build-angular:protractor", 132 | "options": { 133 | "protractorConfig": "apps/dashboard-e2e/protractor.conf.js", 134 | "devServerTarget": "dashboard:serve" 135 | }, 136 | "configurations": { 137 | "production": { 138 | "devServerTarget": "dashboard:serve:production" 139 | } 140 | } 141 | }, 142 | "lint": { 143 | "builder": "@angular-devkit/build-angular:tslint", 144 | "options": { 145 | "tsConfig": "apps/dashboard-e2e/tsconfig.e2e.json", 146 | "exclude": [ 147 | "**/node_modules/**" 148 | ] 149 | } 150 | } 151 | } 152 | }, 153 | "ui-login": { 154 | "root": "libs/ui-login", 155 | "sourceRoot": "libs/ui-login/src", 156 | "projectType": "library", 157 | "prefix": "ui", 158 | "architect": { 159 | "test": { 160 | "builder": "@angular-devkit/build-angular:karma", 161 | "options": { 162 | "main": "libs/ui-login/src/test.ts", 163 | "tsConfig": "libs/ui-login/tsconfig.spec.json", 164 | "karmaConfig": "libs/ui-login/karma.conf.js" 165 | } 166 | }, 167 | "lint": { 168 | "builder": "@angular-devkit/build-angular:tslint", 169 | "options": { 170 | "tsConfig": [ 171 | "libs/ui-login/tsconfig.lib.json", 172 | "libs/ui-login/tsconfig.spec.json" 173 | ], 174 | "exclude": [ 175 | "**/node_modules/**" 176 | ] 177 | } 178 | } 179 | } 180 | }, 181 | "material": { 182 | "root": "libs/material", 183 | "sourceRoot": "libs/material/src", 184 | "projectType": "library", 185 | "prefix": "workshop", 186 | "architect": { 187 | "test": { 188 | "builder": "@angular-devkit/build-angular:karma", 189 | "options": { 190 | "main": "libs/material/src/test.ts", 191 | "tsConfig": "libs/material/tsconfig.spec.json", 192 | "karmaConfig": "libs/material/karma.conf.js" 193 | } 194 | }, 195 | "lint": { 196 | "builder": "@angular-devkit/build-angular:tslint", 197 | "options": { 198 | "tsConfig": [ 199 | "libs/material/tsconfig.lib.json", 200 | "libs/material/tsconfig.spec.json" 201 | ], 202 | "exclude": [ 203 | "**/node_modules/**" 204 | ] 205 | } 206 | } 207 | } 208 | }, 209 | "ui-toolbar": { 210 | "root": "libs/ui-toolbar", 211 | "sourceRoot": "libs/ui-toolbar/src", 212 | "projectType": "library", 213 | "prefix": "workshop", 214 | "architect": { 215 | "test": { 216 | "builder": "@angular-devkit/build-angular:karma", 217 | "options": { 218 | "main": "libs/ui-toolbar/src/test.ts", 219 | "tsConfig": "libs/ui-toolbar/tsconfig.spec.json", 220 | "karmaConfig": "libs/ui-toolbar/karma.conf.js" 221 | } 222 | }, 223 | "lint": { 224 | "builder": "@angular-devkit/build-angular:tslint", 225 | "options": { 226 | "tsConfig": [ 227 | "libs/ui-toolbar/tsconfig.lib.json", 228 | "libs/ui-toolbar/tsconfig.spec.json" 229 | ], 230 | "exclude": [ 231 | "**/node_modules/**" 232 | ] 233 | } 234 | } 235 | } 236 | } 237 | }, 238 | "cli": { 239 | "warnings": { 240 | "typescriptMismatch": false, 241 | "versionMismatch": false 242 | }, 243 | "defaultCollection": "@nrwl/schematics" 244 | }, 245 | "defaultProject": "dashboard", 246 | "schematics": { 247 | "@nrwl/schematics:component": { 248 | "styleext": "scss" 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/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 | './src/**/*.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: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project 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 dashboard!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/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 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/apps/dashboard-e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/dashboard/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /apps/dashboard/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 | const { join } = require('path'); 5 | const getBaseKarmaConfig = require('../../karma.conf'); 6 | 7 | module.exports = function(config) { 8 | const baseConfig = getBaseKarmaConfig(); 9 | config.set({ 10 | ...baseConfig, 11 | coverageIstanbulReporter: { 12 | ...baseConfig.coverageIstanbulReporter, 13 | dir: join(__dirname, '../../coverage/apps/dashboard') 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { LoginComponent } from '@workshop/ui-login'; 4 | 5 | const routes: Routes = [ 6 | {path: '', redirectTo: 'projects', pathMatch: 'full'}, 7 | {path: 'projects', loadChildren: './projects/projects.module#ProjectsModule'}, 8 | {path: 'login', component: LoginComponent}, 9 | {path: '**', redirectTo: '', pathMatch: 'full'} 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes)], 14 | exports: [RouterModule], 15 | providers: [] 16 | }) 17 | export class AppRoutingModule { 18 | } 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | {{link.icon}} 9 | {{link.label}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/app/app.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [AppComponent] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have as title 'dashboard'`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app.title).toEqual('dashboard'); 21 | }); 22 | 23 | it('should render title in a h1 tag', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.debugElement.nativeElement; 27 | expect(compiled.querySelector('h1').textContent).toContain( 28 | 'Welcome to dashboard!' 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from '@workshop/core-data'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent implements OnInit { 12 | title = 'Angular Reactive Workshop'; 13 | isLoggedIn$: Observable = this.authService.isAuthenticated$; 14 | isLoggedIn; 15 | 16 | links = [ 17 | { path: '/projects', icon: 'work', label: 'Projects' } 18 | ]; 19 | 20 | constructor( 21 | private authService: AuthService, 22 | private router: Router 23 | ) { } 24 | 25 | ngOnInit() { 26 | this.isLoggedIn$ 27 | .subscribe(loggedIn => { 28 | const path = (loggedIn) ? '' : 'login'; 29 | this.isLoggedIn = loggedIn; 30 | this.router.navigate([path]); 31 | }) 32 | } 33 | 34 | logout() { 35 | this.authService.logout(); 36 | } 37 | 38 | isSidenaveOpen(component, authentication) { 39 | return component.opened && authentication; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { LayoutModule } from '@angular/cdk/layout'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { NgModule } from '@angular/core'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { BrowserModule } from '@angular/platform-browser'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { NxModule } from '@nrwl/nx'; 8 | 9 | import { CoreDataModule } from '@workshop/core-data'; 10 | import { MaterialModule } from '@workshop/material'; 11 | import { UiLoginModule } from '@workshop/ui-login'; 12 | import { UiToolbarModule } from '@workshop/ui-toolbar'; 13 | 14 | import { AppRoutingModule } from './app-routing.module'; 15 | import { AppComponent } from './app.component'; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | AppComponent 20 | ], 21 | imports: [ 22 | AppRoutingModule, 23 | BrowserModule, 24 | BrowserAnimationsModule, 25 | CoreDataModule, 26 | HttpClientModule, 27 | LayoutModule, 28 | NxModule.forRoot(), 29 | FormsModule, 30 | ReactiveFormsModule, 31 | UiLoginModule, 32 | UiToolbarModule, 33 | MaterialModule, 34 | ], 35 | providers: [], 36 | bootstrap: [AppComponent] 37 | }) 38 | export class AppModule {} 39 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/project-details/project-details.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Editing {{originalTitle}} 6 | Create Project 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{customer.firstName}} {{customer.lastName}} 24 | 25 | 26 | 27 | 28 | 29 | {{selectedProject.percentComplete}}% Completed 30 | 32 | 33 | 34 | 35 | 36 | Approved by Customer 37 | 38 | 39 | 40 | Save 41 | Cancel 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/project-details/project-details.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/app/projects/project-details/project-details.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/project-details/project-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectDetailsComponent } from './project-details.component'; 4 | 5 | describe('ProjectDetailsComponent', () => { 6 | let component: ProjectDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProjectDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProjectDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/project-details/project-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Customer, Project } from '@workshop/core-data'; 3 | 4 | @Component({ 5 | selector: 'app-project-details', 6 | templateUrl: './project-details.component.html', 7 | styleUrls: ['./project-details.component.scss'] 8 | }) 9 | export class ProjectDetailsComponent { 10 | originalTitle: string; 11 | selectedProject: Project; 12 | @Output() saved = new EventEmitter(); 13 | @Output() cancelled = new EventEmitter(); 14 | 15 | @Input() customers: Customer[]; 16 | @Input() set project(value: Project) { 17 | if (value) { this.originalTitle = value.title; } 18 | this.selectedProject = Object.assign({}, value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects-list/projects-list.animations.ts: -------------------------------------------------------------------------------- 1 | import { animate, group, query, stagger, style, transition, trigger } from '@angular/animations'; 2 | 3 | export const LIST_ANIMATION = trigger('listAnimation', [ 4 | transition(':enter, :leave, * => pending', []), 5 | transition('* => *', [ 6 | // animate both the newly entered and removed items on the page 7 | // at the same time 8 | group([ 9 | query(':enter', [ 10 | style({ opacity: 0, height: '0px' }), 11 | stagger('50ms', [ 12 | animate('500ms cubic-bezier(.35,0,.25,1)', style('*')) 13 | ]) 14 | ], { optional: true }), 15 | 16 | query(':leave', [ 17 | stagger('50ms', [ 18 | animate('500ms cubic-bezier(.35,0,.25,1)', style({ opacity: 0, height: '0px', borderTop: 0, borderBottom: 0 })) 19 | ]) 20 | ], { optional: true }) 21 | ]), 22 | ]), 23 | ]) 24 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects-list/projects-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Projects 5 | 6 | 7 | 8 | 9 | 10 | {{project.title}} 11 | 12 | {{project.details}} 13 | 14 | 15 | close 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects-list/projects-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/app/projects/projects-list/projects-list.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects-list/projects-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectsListComponent } from './projects-list.component'; 4 | 5 | describe('ProjectsListComponent', () => { 6 | let component: ProjectsListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProjectsListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProjectsListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects-list/projects-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { Project } from '@workshop/core-data'; 3 | 4 | import { LIST_ANIMATION } from './projects-list.animations'; 5 | 6 | @Component({ 7 | selector: 'app-projects-list', 8 | templateUrl: './projects-list.component.html', 9 | styleUrls: ['./projects-list.component.scss'], 10 | animations: [LIST_ANIMATION] 11 | }) 12 | export class ProjectsListComponent implements OnInit { 13 | @Input() projects: Project[]; 14 | @Input() readonly = false; 15 | @Output() selected = new EventEmitter(); 16 | @Output() deleted = new EventEmitter(); 17 | 18 | animationsDisabled = true; 19 | 20 | trackProject(index, project) { 21 | return project.id; 22 | } 23 | 24 | ngOnInit() { 25 | setTimeout(() => { 26 | this.animationsDisabled = false; 27 | }, 500) 28 | } 29 | 30 | prepareListState() { 31 | return this.projects ? this.projects.length : 'pending'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { ProjectsComponent } from './projects.component'; 5 | 6 | const routes: Routes = [ 7 | { path: '', component: ProjectsComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class ProjectsRoutingModule { } 15 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/app/projects/projects.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectsComponent } from './projects.component'; 4 | 5 | describe('ProjectsComponent', () => { 6 | let component: ProjectsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProjectsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProjectsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Project, ProjectsFacade, Customer, CustomersFacade } from '@workshop/core-data'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-projects', 7 | templateUrl: './projects.component.html', 8 | styleUrls: ['./projects.component.scss'] 9 | }) 10 | export class ProjectsComponent implements OnInit { 11 | customers$: Observable = this.customersFacade.allCustomers$; 12 | projects$: Observable = this.projectsFacade.allProjects$; 13 | currentProject$: Observable = this.projectsFacade.currentProject$; 14 | 15 | constructor( 16 | private projectsFacade: ProjectsFacade, 17 | private customersFacade: CustomersFacade 18 | ) { } 19 | 20 | ngOnInit() { 21 | this.customersFacade.loadCustomers(); 22 | this.projectsFacade.loadProjects(); 23 | this.projectsFacade.mutations$.subscribe(_ => this.resetCurrentProject()); 24 | this.resetCurrentProject(); 25 | } 26 | 27 | resetCurrentProject() { 28 | this.selectProject({id: null}); 29 | } 30 | 31 | selectProject(project) { 32 | this.projectsFacade.selectProject(project.id); 33 | } 34 | 35 | saveProject(project) { 36 | if (!project.id) { 37 | this.projectsFacade.addProject(project); 38 | } else { 39 | this.projectsFacade.updateProject(project); 40 | } 41 | } 42 | 43 | deleteProject(project) { 44 | this.projectsFacade.deleteProject(project); 45 | } 46 | // projects$: Observable; 47 | // customers$: Observable; 48 | // currentProject: Project; 49 | 50 | // constructor( 51 | // private projectsService: ProjectsService, 52 | // private customerService: CustomersService, 53 | // private ns: NotificationsService) { } 54 | 55 | // ngOnInit() { 56 | // this.getProjects(); 57 | // this.getCustomers(); 58 | // this.resetCurrentProject(); 59 | // } 60 | 61 | // resetCurrentProject() { 62 | // this.currentProject = emptyProject; 63 | // } 64 | 65 | // selectProject(project) { 66 | // this.currentProject = project; 67 | // } 68 | 69 | // cancel(project) { 70 | // this.resetCurrentProject(); 71 | // } 72 | 73 | // getCustomers() { 74 | // this.customers$ = this.customerService.all(); 75 | // } 76 | 77 | // getProjects() { 78 | // this.projects$ = this.projectsService.all(); 79 | // } 80 | 81 | // saveProject(project) { 82 | // if (!project.id) { 83 | // this.createProject(project); 84 | // } else { 85 | // this.updateProject(project); 86 | // } 87 | // } 88 | 89 | // createProject(project) { 90 | // this.projectsService.create(project) 91 | // .subscribe(response => { 92 | // this.ns.emit('Project created!'); 93 | // this.getProjects(); 94 | // this.resetCurrentProject(); 95 | // }); 96 | // } 97 | 98 | // updateProject(project) { 99 | // this.projectsService.update(project) 100 | // .subscribe(response => { 101 | // this.ns.emit('Project saved!'); 102 | // this.getProjects(); 103 | // this.resetCurrentProject(); 104 | // }); 105 | // } 106 | 107 | // deleteProject(project) { 108 | // this.projectsService.delete(project) 109 | // .subscribe(response => { 110 | // this.ns.emit('Project deleted!'); 111 | // this.getProjects(); 112 | // this.resetCurrentProject(); 113 | // }); 114 | // } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { ProjectsModule } from './projects.module'; 2 | 3 | describe('ProjectsModule', () => { 4 | let projectsModule: ProjectsModule; 5 | 6 | beforeEach(() => { 7 | projectsModule = new ProjectsModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(projectsModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/projects/projects.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ProjectsRoutingModule } from './projects-routing.module'; 5 | import { ProjectsComponent } from './projects.component'; 6 | import { ProjectsListComponent } from './projects-list/projects-list.component'; 7 | import { ProjectDetailsComponent } from './project-details/project-details.component'; 8 | import { MaterialModule } from '@workshop/material'; 9 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | ProjectsRoutingModule, 15 | MaterialModule, 16 | ReactiveFormsModule, 17 | FormsModule 18 | ], 19 | declarations: [ 20 | ProjectsComponent, 21 | ProjectsListComponent, 22 | ProjectDetailsComponent, 23 | ] 24 | }) 25 | export class ProjectsModule { } 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/dashboard/src/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/assets/background.jpg -------------------------------------------------------------------------------- /apps/dashboard/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/assets/logo.png -------------------------------------------------------------------------------- /apps/dashboard/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiEndpoint: 'http://localhost:3000' 4 | }; 5 | -------------------------------------------------------------------------------- /apps/dashboard/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | apiEndpoint: 'http://localhost:3000/' 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/apps/dashboard/src/favicon.ico -------------------------------------------------------------------------------- /apps/dashboard/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/dashboard/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() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /apps/dashboard/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 | /** Evergreen browsers require these. **/ 44 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 45 | 46 | 47 | /** 48 | * Web Animations `@angular/platform-browser/animations` 49 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 50 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 51 | **/ 52 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 53 | 54 | /** 55 | * By default, zone.js will patch all possible macroTask and DomEvents 56 | * user can disable parts of macroTask/DomEvents patch by setting following flags 57 | */ 58 | 59 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 60 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 61 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 62 | 63 | /* 64 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 65 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 66 | */ 67 | // (window as any).__Zone_enable_cross_context_check = true; 68 | 69 | /*************************************************************************************************** 70 | * Zone JS is required by default for Angular itself. 71 | */ 72 | import 'zone.js/dist/zone'; // Included with Angular CLI. 73 | 74 | /*************************************************************************************************** 75 | * APPLICATION IMPORTS 76 | */ 77 | 78 | import 'hammerjs'; 79 | -------------------------------------------------------------------------------- /apps/dashboard/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 2 | html { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | font-family: Roboto, sans-serif; 9 | height: 100%; 10 | display: flex; 11 | } 12 | 13 | md-toolbar-row { 14 | justify-content: space-between; 15 | } 16 | 17 | p { 18 | margin: 16px; 19 | } 20 | 21 | [md-raised-button] { 22 | width: 100%; 23 | } 24 | 25 | md-grid-list { 26 | max-width: 1403px; 27 | margin: 16px; 28 | } 29 | 30 | md-sidenav-layout { 31 | height: 100vh; 32 | } 33 | 34 | md-sidenav { 35 | width: 200px; 36 | } 37 | 38 | md-sidenav a { 39 | box-sizing: border-box; 40 | display: block; 41 | font-size: 14px; 42 | font-weight: 400; 43 | line-height: 47px; 44 | text-decoration: none; 45 | -webkit-transition: all .3s; 46 | transition: all .3s; 47 | padding: 0 16px; 48 | position: relative; 49 | } 50 | 51 | .icon-20 { 52 | font-size: 20px; 53 | } 54 | 55 | * { 56 | -webkit-font-smoothing: antialiased; 57 | -moz-osx-font-smoothing: grayscale; 58 | } 59 | 60 | .container { 61 | display: flex; 62 | margin: 10px; 63 | flex-wrap: wrap; 64 | } 65 | 66 | .container [class*="col"] { 67 | padding: 10px; 68 | flex: 1; 69 | } 70 | 71 | md-card-header .mat-card-header-text { 72 | margin-left: 0; 73 | border-bottom: 1px solid #ffd740; 74 | } 75 | 76 | md-card-title h1 { 77 | display: inline; 78 | } 79 | 80 | .header { 81 | padding-bottom: 10px; 82 | color: #673ab7; 83 | border-bottom: 2px solid #673ab7; 84 | } 85 | 86 | app-root { 87 | display: flex; 88 | flex-direction: column; 89 | flex: 1; 90 | } 91 | 92 | mat-toolbar { 93 | 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); 94 | z-index: 1; 95 | } 96 | 97 | mat-sidenav { 98 | box-shadow: 3px 0 6px rgba(0, 0, 0, .24); 99 | width: 200px; 100 | } 101 | 102 | mat-card:not(.dashboard-card) { 103 | margin-bottom: 20px; 104 | } 105 | 106 | .mat-sidenav-container { 107 | background: #f5f5f5; 108 | flex: 1; 109 | } 110 | 111 | .fill-remaining-space { 112 | flex: 1 1 auto; 113 | } 114 | 115 | .full-width { 116 | width: 100%; 117 | } 118 | 119 | .dashboard-card { 120 | position: absolute !important; 121 | top: 15px; 122 | left: 15px; 123 | right: 15px; 124 | bottom: 15px; 125 | mat-card-header { 126 | justify-content: center; 127 | } 128 | } 129 | 130 | .dashboard-card-content { 131 | text-align: center; 132 | } 133 | 134 | .more-button { 135 | position: absolute; 136 | top: 5px; 137 | right: 10px; 138 | } 139 | 140 | .grid-container { 141 | margin: 20px; 142 | } 143 | 144 | .list-container { 145 | margin: 20px; 146 | .full-width-table { 147 | width: 100%; 148 | } 149 | tr { 150 | -webkit-transition: background-color 300ms; 151 | -moz-transition: background-color 300ms; 152 | -ms-transition: background-color 300ms; 153 | -o-transition: background-color 300ms; 154 | transition: background-color 300ms; 155 | } 156 | tr:not(.mat-header-row):hover { 157 | background: whitesmoke; 158 | cursor: pointer; 159 | } 160 | } 161 | 162 | .details-container { 163 | mat-form-field { 164 | width: 100%; 165 | } 166 | mat-card-header { 167 | display: flex; 168 | justify-content: space-between; 169 | align-items: center; 170 | } 171 | } 172 | 173 | mat-list-item:not(:first-of-type) { 174 | border-top: 1px solid #efefef; 175 | } 176 | 177 | mat-form-field { 178 | width: 100%; 179 | } 180 | 181 | mat-list-item:hover { 182 | cursor: pointer; 183 | background: whitesmoke; 184 | } 185 | 186 | mat-list-item:not(:first-of-type) { 187 | border-top: 1px solid #efefef; 188 | } 189 | 190 | .symbol { 191 | color: #777; 192 | } 193 | 194 | mat-card-actions { 195 | margin-bottom: 0; 196 | } 197 | 198 | .full-width { 199 | width: 100%; 200 | } 201 | 202 | .nav-link { 203 | color: rgba(0, 0, 0, .54); 204 | display: flex !important; 205 | align-items: center; 206 | padding-top: 5px; 207 | padding-bottom: 5px; 208 | } 209 | 210 | // UI LOGIN 211 | ui-login { 212 | height: 100%; 213 | .background { 214 | position: fixed; 215 | left: 0; 216 | right: 0; 217 | z-index: 1; 218 | display: block; 219 | background: url(assets/background.jpg) no-repeat center center fixed; 220 | background-size: cover; 221 | height: 100%; 222 | overflow: hidden; 223 | } 224 | .container { 225 | position: fixed; 226 | left: 0; 227 | right: 0; 228 | top: 0; 229 | bottom: 0; 230 | z-index: 100; 231 | .card-wrapper { 232 | margin: auto; 233 | display: flex; 234 | justify-content: center; 235 | } 236 | } 237 | mat-card { 238 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 239 | mat-card-header { 240 | margin-bottom: 10px; 241 | } 242 | mat-form-field { 243 | width: 100%; 244 | } 245 | } 246 | .ig-xpro2 { 247 | -webkit-filter: contrast(1.3) brightness(0.8) sepia(0.3) saturate(1.5) hue-rotate(-20deg); 248 | filter: contrast(1.3) brightness(0.8) sepia(0.3) saturate(1.5) hue-rotate(-20deg); 249 | } 250 | .ig-willow { 251 | -webkit-filter: saturate(0.02) contrast(0.85) brightness(1.2) sepia(0.02); 252 | filter: saturate(0.02) contrast(0.85) brightness(1.2) sepia(0.02); 253 | } 254 | .ig-walden { 255 | -webkit-filter: sepia(0.35) contrast(0.9) brightness(1.1) hue-rotate(-10deg) saturate(1.5); 256 | filter: sepia(0.35) contrast(0.9) brightness(1.1) hue-rotate(-10deg) saturate(1.5); 257 | } 258 | .ig-valencia { 259 | -webkit-filter: sepia(0.15) saturate(1.5) contrast(0.9); 260 | filter: sepia(0.15) saturate(1.5) contrast(0.9); 261 | } 262 | .ig-toaster { 263 | -webkit-filter: sepia(0.4) saturate(2.5) hue-rotate(-30deg) contrast(0.67); 264 | -filter: sepia(0.4) saturate(2.5) hue-rotate(-30deg) contrast(0.67); 265 | } 266 | .ig-sutro { 267 | -webkit-filter: brightness(0.75) contrast(1.3) sepia(0.5) hue-rotate(-25deg); 268 | filter: brightness(0.75) contrast(1.3) sepia(0.5) hue-rotate(-25deg); 269 | } 270 | .ig-sierra { 271 | -webkit-filter: contrast(0.8) saturate(1.2) sepia(0.15); 272 | filter: contrast(0.8) saturate(1.2) sepia(0.15); 273 | } 274 | .ig-rise { 275 | -webkit-filter: saturate(1.4) sepia(0.25) hue-rotate(-15deg) contrast(0.8) brightness(1.1); 276 | filter: saturate(1.4) sepia(0.25) hue-rotate(-15deg) contrast(0.8) brightness(1.1); 277 | } 278 | .ig-nashville { 279 | -webkit-filter: sepia(0.4) saturate(1.5) contrast(0.9) brightness(1.1) hue-rotate(-15deg); 280 | filter: sepia(0.4) saturate(1.5) contrast(0.9) brightness(1.1) hue-rotate(-15deg); 281 | } 282 | .ig-mayfair { 283 | -webkit-filter: saturate(1.4) contrast(1.1); 284 | filter: saturate(1.4) contrast(1.1); 285 | } 286 | .ig-lofi { 287 | filter: contrast(1.4) brightness(0.9) sepia(0.05); 288 | -webkit-filter: contrast(1.4) brightness(0.9) sepia(0.05); 289 | } 290 | .ig-kelvin { 291 | filter: sepia(0.4) saturate(2.4) brightness(1.3) contrast(1); 292 | -webkit-filter: sepia(0.4) saturate(2.4) brightness(1.3) contrast(1); 293 | } 294 | .ig-inkwell { 295 | -webkit-filter: grayscale(1) brightness(1.2) contrast(1.05); 296 | filter: grayscale(1) brightness(1.2) contrast(1.05); 297 | } 298 | .ig-hudson { 299 | -webkit-filter: contrast(1.2) brightness(0.9) hue-rotate(-10deg); 300 | filter: contrast(1.2) brightness(0.9) hue-rotate(-10deg); 301 | } 302 | .hefe { 303 | -webkit-filter: contrast(1.3) sepia(0.3) saturate(1.3) hue-rotate(-10deg) brightness(0.95); 304 | filter: contrast(1.3) sepia(0.3) saturate(1.3) hue-rotate(-10deg) brightness(0.95); 305 | } 306 | .ig-earlybird { 307 | -webkit-filter: sepia(0.4) saturate(1.6) contrast(1.1) brightness(0.9) hue-rotate(-10deg); 308 | filter: sepia(0.4) saturate(1.6) contrast(1.1) brightness(0.9) hue-rotate(-10deg); 309 | } 310 | .ig-brannan { 311 | -webkit-filter: sepia(0.5) contrast(1.4); 312 | filter: sepia(0.5) contrast(1.4); 313 | } 314 | .ig-amaro { 315 | -webkit-filter: hue-rotate(-10deg) contrast(0.9) brightness(1.1) saturate(1.5); 316 | filter: hue-rotate(-10deg) contrast(0.9) brightness(1.1) saturate(1.5); 317 | } 318 | .ig-1977 { 319 | -webkit-filter: sepia(0.5) hue-rotate(-30deg) saturate(1.2) contrast(0.8); 320 | filter: sepia(0.5) hue-rotate(-30deg) saturate(1.2) contrast(0.8); 321 | } 322 | } 323 | 324 | // UI TOOLBAR 325 | mat-toolbar { 326 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 327 | z-index: 900; 328 | >.mat-mini-fab { 329 | margin-right: 10px; 330 | } 331 | .spacer { 332 | flex: 1 1 auto; 333 | } 334 | .title { 335 | vertical-align: middle; 336 | } 337 | .logo { 338 | margin-left: 20px; 339 | img { 340 | vertical-align: middle; 341 | width: 100px; 342 | height: auto; 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /apps/dashboard/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/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /apps/dashboard/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/apps/dashboard", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ], 11 | "include": [ 12 | "**/*.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/dashboard/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/apps/dashboard", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/dashboard/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 | const { join } = require('path'); 5 | const { constants } = require('karma'); 6 | 7 | module.exports = () => { 8 | return { 9 | basePath: '', 10 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 11 | plugins: [ 12 | require('karma-jasmine'), 13 | require('karma-chrome-launcher'), 14 | require('karma-jasmine-html-reporter'), 15 | require('karma-coverage-istanbul-reporter'), 16 | require('@angular-devkit/build-angular/plugins/karma') 17 | ], 18 | client: { 19 | clearContext: false // leave Jasmine Spec Runner output visible in browser 20 | }, 21 | coverageIstanbulReporter: { 22 | dir: join(__dirname, '../../coverage'), 23 | reports: ['html', 'lcovonly'], 24 | fixWebpackSourcePaths: true 25 | }, 26 | reporters: ['progress', 'kjhtml'], 27 | port: 9876, 28 | colors: true, 29 | logLevel: constants.LOG_INFO, 30 | autoWatch: true, 31 | browsers: ['Chrome'], 32 | singleRun: true 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/libs/.gitkeep -------------------------------------------------------------------------------- /libs/core-data/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 | const { join } = require('path'); 5 | const getBaseKarmaConfig = require('../../karma.conf'); 6 | 7 | module.exports = function(config) { 8 | const baseConfig = getBaseKarmaConfig(); 9 | config.set({ 10 | ...baseConfig, 11 | coverageIstanbulReporter: { 12 | ...baseConfig.coverageIstanbulReporter, 13 | dir: join(__dirname, '../../coverage/libs/core-data') 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /libs/core-data/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthGuardService } from './lib/auth/auth-guard.service'; 2 | export { AuthService } from './lib/auth/auth.service'; 3 | export { CoreDataModule } from './lib/core-data.module'; 4 | export { NotificationsService } from './lib/notifications/notifications.service'; 5 | export { CustomersService } from './lib/customers/customers.service'; 6 | export { Customer } from './lib/customers/customer.model'; 7 | export { Project } from './lib/projects/project.model'; 8 | export { ProjectsService } from './lib/projects/projects.service'; 9 | export { ProjectsFacade } from './lib/state/projects/projects.facade'; 10 | export { CustomersFacade } from './lib/state/customers/customers.facade'; 11 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/auth/auth-guard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuardService } from './auth-guard.service'; 4 | 5 | describe('AuthGuardService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthGuardService = TestBed.get(AuthGuardService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/auth/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @Injectable() 6 | export class AuthGuardService implements CanActivate { 7 | 8 | constructor(public auth: AuthService, public router: Router) { } 9 | 10 | canActivate(): boolean { 11 | if (!this.auth.isAuthenticated$.value) { 12 | this.router.navigate(['login']); 13 | return false; 14 | } 15 | return true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthService = TestBed.get(AuthService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { environment } from '@env/environment'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthService { 10 | model = 'auth/login' 11 | isAuthenticated$ = new BehaviorSubject(false); 12 | 13 | constructor(private http: HttpClient) { 14 | this.setToken(this.getToken()); 15 | } 16 | 17 | getUrl() { 18 | return `${environment.apiEndpoint}${this.model}`; 19 | } 20 | 21 | login(email, password) { 22 | return this.http.post(this.getUrl(), { email, password}); 23 | } 24 | 25 | logout() { 26 | this.setToken(''); 27 | } 28 | 29 | // TOKEN 30 | setToken(token) { 31 | localStorage.setItem('token', token); 32 | this.isAuthenticated$.next(token !== ''); // Could be more Robust 33 | } 34 | 35 | getToken() { 36 | return localStorage.getItem('token'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/auth/token-interceptor.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TokenInterceptor } from './token.interceptor'; 4 | 5 | describe('TokenInterceptor', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: TokenInterceptor = TestBed.get(TokenInterceptor); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/auth/token.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { tap } from 'rxjs/operators'; 2 | import { Observable } from 'rxjs'; 3 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; 4 | import { Injectable } from '@angular/core'; 5 | 6 | import { AuthService } from './auth.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class TokenInterceptor implements HttpInterceptor { 12 | constructor(public auth: AuthService) { } 13 | 14 | // intercept request and add token 15 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 16 | 17 | // modify request 18 | request = request.clone({ 19 | setHeaders: { 20 | Authorization: `Bearer ${this.auth.getToken()}` 21 | } 22 | }); 23 | 24 | console.log('---- request ----'); 25 | console.log(request); 26 | console.log('--- end of request ---'); 27 | 28 | return next.handle(request) 29 | .pipe( 30 | tap(event => { 31 | if (event instanceof HttpResponse) { 32 | console.log(' all looks good'); 33 | // http response status code 34 | console.log(event.status); 35 | } 36 | }, error => { 37 | // http response status code 38 | console.log('---- response ----'); 39 | console.error('status code:'); 40 | console.error(error.status); 41 | console.error(error.message); 42 | console.log('--- end of response ---'); 43 | }) 44 | ) 45 | }; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/core-data.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { CoreDataModule } from './core-data.module'; 3 | 4 | describe('CoreDataModule', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [CoreDataModule] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create', () => { 12 | expect(CoreDataModule).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/core-data.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | import { AuthGuardService } from './auth/auth-guard.service'; 6 | import { AuthService } from './auth/auth.service'; 7 | import { CustomersService } from './customers/customers.service'; 8 | import { NotificationsService } from './notifications/notifications.service'; 9 | import { ProjectsService } from './projects/projects.service'; 10 | import { StateModule } from './state/state.module'; 11 | 12 | @NgModule({ 13 | providers: [ 14 | AuthService, 15 | AuthGuardService, 16 | NotificationsService, 17 | CustomersService, 18 | ProjectsService 19 | ], 20 | imports: [ 21 | CommonModule, 22 | HttpClientModule, 23 | StateModule 24 | ], 25 | }) 26 | export class CoreDataModule {} 27 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/customers/customer.model.ts: -------------------------------------------------------------------------------- 1 | export interface Customer { 2 | id: string; 3 | firstName: string; 4 | lastName: string; 5 | email: string; 6 | phone: string; 7 | title: string; 8 | status: Status 9 | } 10 | 11 | export enum Status { 12 | WON = 1, 13 | PROSPECT = 2, 14 | QUEUED = 3, 15 | LOST = 4 16 | } 17 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/customers/customers.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CustomersService } from './customers.service'; 4 | 5 | describe('CustomersService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: CustomersService = TestBed.get(CustomersService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/customers/customers.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { environment } from '@env/environment'; 4 | 5 | import { Customer } from './customer.model'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class CustomersService { 11 | model = 'customers' 12 | 13 | constructor(private http: HttpClient) { } 14 | 15 | getUrl() { 16 | return `${environment.apiEndpoint}${this.model}`; 17 | } 18 | 19 | getUrlForId(id) { 20 | return `${this.getUrl()}/${id}`; 21 | } 22 | 23 | all() { 24 | return this.http.get(this.getUrl()); 25 | } 26 | 27 | load(id) { 28 | return this.http.get(this.getUrlForId(id)); 29 | } 30 | 31 | create(customer: Customer) { 32 | return this.http.post(this.getUrl(), customer); 33 | } 34 | 35 | update(customer: Customer) { 36 | return this.http.patch(this.getUrl(), customer); 37 | } 38 | 39 | delete(customer: Customer) { 40 | return this.http.delete(this.getUrlForId(customer.id)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/error/error-interceptor.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ErrorInterceptor } from './error.interceptor'; 4 | 5 | describe('ErrorInterceptorService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ErrorInterceptor = TestBed.get(ErrorInterceptor); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/error/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { tap, catchError } from 'rxjs/operators'; 5 | import { NotificationsService } from '../notifications/notifications.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class ErrorInterceptor implements HttpInterceptor { 11 | 12 | constructor(private notificationsService: NotificationsService) { } 13 | 14 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 15 | return next.handle(request) 16 | .pipe( 17 | catchError(error => { 18 | if (error instanceof HttpErrorResponse) { 19 | this.notificationsService.emit(`${request.url} | ${error.status} – ${error.statusText}`); 20 | } 21 | return throwError(error); 22 | }) 23 | ) 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/notifications/notifications.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material'; 3 | 4 | @Injectable() 5 | export class NotificationsService { 6 | 7 | constructor(private snackbar: MatSnackBar) { 8 | } 9 | 10 | emit(notification) { 11 | this.snackbar.open(notification, 'OK', { duration: 3000 }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/projects/project.model.ts: -------------------------------------------------------------------------------- 1 | export interface Project { 2 | id: string; 3 | title: string; 4 | details: string; 5 | percentComplete: number; 6 | approved: boolean; 7 | customerId: string; 8 | } 9 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/projects/projects.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectsService } from './projects.service'; 4 | 5 | describe('ProjectsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ProjectsService = TestBed.get(ProjectsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/projects/projects.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { of, throwError } from 'rxjs'; 4 | import { switchMap, catchError } from 'rxjs/operators'; 5 | import { environment } from '@env/environment'; 6 | import { Project } from './project.model'; 7 | import { NotificationsService } from '../notifications/notifications.service'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ProjectsService { 13 | model = 'projects' 14 | 15 | constructor( 16 | private http: HttpClient, 17 | private notificationsService: NotificationsService 18 | ) { } 19 | 20 | getUrl() { 21 | return `${environment.apiEndpoint}${this.model}`; 22 | } 23 | 24 | getUrlForId(id) { 25 | return `${this.getUrl()}/${id}`; 26 | } 27 | 28 | all() { 29 | return this.http.get(this.getUrl()); 30 | } 31 | 32 | load(id) { 33 | return this.http.get(this.getUrlForId(id)); 34 | } 35 | 36 | loadByCustomer(customerId: string) { 37 | return this.http.get(this.getUrl(), {params: {customerId}}) 38 | .pipe( 39 | switchMap(projects => { 40 | if (projects.length) { 41 | return of(projects); 42 | } else { 43 | return throwError(`No projects exist for customer with ID ${customerId}`); 44 | } 45 | }), 46 | catchError(error => { 47 | this.notificationsService.emit(error); 48 | 49 | return throwError(error); 50 | }) 51 | ) 52 | } 53 | 54 | create(project: Project) { 55 | return this.http.post(this.getUrl(), project); 56 | } 57 | 58 | update(project: Project) { 59 | return this.http.patch(this.getUrlForId(project.id), project); 60 | } 61 | 62 | delete(project: Project) { 63 | return this.http.delete(this.getUrlForId(project.id)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/customers/customers.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum CustomersActionTypes { 4 | LoadCustomers = '[Customers] Load Data', 5 | CustomersLoaded = '[Customers] Data Loaded' 6 | } 7 | 8 | export class LoadCustomers implements Action { 9 | readonly type = CustomersActionTypes.LoadCustomers; 10 | constructor() {} 11 | } 12 | 13 | export class CustomersLoaded implements Action { 14 | readonly type = CustomersActionTypes.CustomersLoaded; 15 | constructor(public payload: any) {} 16 | } 17 | 18 | export type CustomersActions = LoadCustomers | CustomersLoaded; 19 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/customers/customers.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect } from '@ngrx/effects'; 3 | import { DataPersistence } from '@nrwl/nx'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { Customer } from '../../customers/customer.model'; 7 | import { CustomersService } from '../../customers/customers.service'; 8 | import { CustomersActionTypes, CustomersLoaded, LoadCustomers } from './customers.actions'; 9 | import { CustomersState } from './customers.reducer'; 10 | 11 | @Injectable() 12 | export class CustomersEffects { 13 | @Effect() 14 | loadCustomers$ = this.dataPersistence.fetch(CustomersActionTypes.LoadCustomers, { 15 | run: (action: LoadCustomers, state: CustomersState) => { 16 | return this.customersService.all().pipe(map((res: Customer[]) => new CustomersLoaded(res))) 17 | }, 18 | 19 | onError: (action: LoadCustomers, error) => { 20 | console.error('Error', error); 21 | } 22 | }); 23 | 24 | constructor( 25 | private actions$: Actions, 26 | private dataPersistence: DataPersistence, 27 | private customersService: CustomersService 28 | ) {} 29 | } 30 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/customers/customers.facade.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { select, Store } from '@ngrx/store'; 3 | 4 | import * as CustomersActions from './customers.actions'; 5 | import { CustomersState } from './customers.reducer'; 6 | import { selectAllCustomers } from '..'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class CustomersFacade { 12 | allCustomers$ = this.store.pipe(select(selectAllCustomers)); 13 | 14 | constructor(private store: Store) {} 15 | 16 | loadCustomers() { 17 | this.store.dispatch(new CustomersActions.LoadCustomers()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/customers/customers.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; 2 | 3 | import { Customer } from '../../customers/customer.model'; 4 | import { CustomersActions, CustomersActionTypes } from './customers.actions'; 5 | 6 | /** 7 | * Interface to the part of the Store containing CustomersState 8 | * and other information related to Customer. 9 | */ 10 | export interface CustomersState extends EntityState {} 11 | 12 | export const adapter: EntityAdapter = createEntityAdapter(); 13 | export const initialState: CustomersState = adapter.getInitialState(); 14 | 15 | export function customersReducer( 16 | state = initialState, 17 | action: CustomersActions 18 | ): CustomersState { 19 | switch (action.type) { 20 | case CustomersActionTypes.CustomersLoaded: { 21 | return adapter.addAll(action.payload, state); 22 | } 23 | 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | // get the selectors 30 | const { selectIds, selectEntities, selectAll } = adapter.getSelectors(); 31 | 32 | // select the array of widget ids 33 | export const selectCustomerIds = selectIds; 34 | 35 | // select the dictionary of widget entities 36 | export const selectCustomerEntities = selectEntities; 37 | 38 | // select the array of widgets 39 | export const selectAllCustomers = selectAll; 40 | 41 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap, createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import * as fromProjects from './projects/projects.reducer'; 4 | import * as fromCustomers from './customers/customers.reducer'; 5 | 6 | import { Project } from '../projects/project.model'; 7 | 8 | export interface AppState { 9 | customers: fromCustomers.CustomersState, 10 | projects: fromProjects.ProjectsState 11 | } 12 | 13 | export const reducers: ActionReducerMap = { 14 | customers: fromCustomers.customersReducer, 15 | projects: fromProjects.projectsReducer 16 | }; 17 | 18 | // ------------------------------------------------------------------- 19 | // PROJECTS SELECTORS 20 | // ------------------------------------------------------------------- 21 | export const selectProjectsState = createFeatureSelector('projects'); 22 | 23 | export const selectProjectIds = createSelector( 24 | selectProjectsState, 25 | fromProjects.selectProjectIds 26 | ); 27 | export const selectProjectEntities = createSelector( 28 | selectProjectsState, 29 | fromProjects.selectProjectEntities 30 | ); 31 | export const selectAllProjects = createSelector( 32 | selectProjectsState, 33 | fromProjects.selectAllProjects 34 | ); 35 | export const selectCurrentProjectId = createSelector( 36 | selectProjectsState, 37 | fromProjects.getSelectedProjectId 38 | ); 39 | 40 | const emptyProject: Project = { 41 | id: null, 42 | title: '', 43 | details: '', 44 | percentComplete: 0, 45 | approved: false, 46 | customerId: null 47 | } 48 | 49 | export const selectCurrentProject = createSelector( 50 | selectProjectEntities, 51 | selectCurrentProjectId, 52 | (projectEntities, projectId) => { 53 | return projectId ? projectEntities[projectId] : emptyProject; 54 | } 55 | ); 56 | 57 | // ------------------------------------------------------------------- 58 | // CUSTOMERS SELECTORS 59 | // ------------------------------------------------------------------- 60 | export const selectCustomersState = createFeatureSelector('customers'); 61 | 62 | export const selectAllCustomers = createSelector( 63 | selectCustomersState, 64 | fromCustomers.selectAllCustomers 65 | ); 66 | 67 | export const selectCustomersProjects = createSelector( 68 | selectAllCustomers, 69 | selectAllProjects, 70 | (customers, projects) => { 71 | return customers.map(customer => ({ 72 | ...customer, 73 | projects: projects.filter(project => project.customerId === customer.id) 74 | })); 75 | } 76 | ); 77 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/projects/projects.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Project } from '@workshop/core-data'; 3 | 4 | export enum ProjectsActionTypes { 5 | ProjectsAction = '[Projects] Action', 6 | ProjectSelected = '[Projects] Selected', 7 | LoadProjects = '[Projects] Load Data', 8 | ProjectsLoaded = '[Projects] Data Loaded', 9 | AddProject = '[Projects] Add Data', 10 | ProjectAdded = '[Projects] Data Added', 11 | UpdateProject = '[Projects] Update Data', 12 | ProjectUpdated = '[Projects] Data Updated', 13 | DeleteProject = '[Projects] Delete Data', 14 | ProjectDeleted = '[Projects] Data Deleted', 15 | } 16 | 17 | export class Projects implements Action { 18 | readonly type = ProjectsActionTypes.ProjectsAction; 19 | } 20 | 21 | export class ProjectSelected implements Action { 22 | readonly type = ProjectsActionTypes.ProjectSelected; 23 | constructor(public payload) { } 24 | } 25 | 26 | export class LoadProjects implements Action { 27 | readonly type = ProjectsActionTypes.LoadProjects; 28 | constructor() { } 29 | } 30 | 31 | export class ProjectsLoaded implements Action { 32 | readonly type = ProjectsActionTypes.ProjectsLoaded; 33 | constructor(public payload: Project[]) { } 34 | } 35 | 36 | export class AddProject implements Action { 37 | readonly type = ProjectsActionTypes.AddProject; 38 | constructor(public payload: Project) { } 39 | } 40 | 41 | export class ProjectAdded implements Action { 42 | readonly type = ProjectsActionTypes.ProjectAdded; 43 | constructor(public payload: Project) { } 44 | } 45 | 46 | export class UpdateProject implements Action { 47 | readonly type = ProjectsActionTypes.UpdateProject; 48 | constructor(public payload: Project) { } 49 | } 50 | 51 | export class ProjectUpdated implements Action { 52 | readonly type = ProjectsActionTypes.ProjectUpdated; 53 | constructor(public payload: Project) { } 54 | } 55 | 56 | export class DeleteProject implements Action { 57 | readonly type = ProjectsActionTypes.DeleteProject; 58 | constructor(public payload: Project) { } 59 | } 60 | 61 | export class ProjectDeleted implements Action { 62 | readonly type = ProjectsActionTypes.ProjectDeleted; 63 | constructor(public payload: Project) { } 64 | } 65 | 66 | export type ProjectsActions = Projects 67 | | ProjectSelected 68 | | LoadProjects 69 | | ProjectsLoaded 70 | | AddProject 71 | | ProjectAdded 72 | | UpdateProject 73 | | ProjectUpdated 74 | | DeleteProject 75 | | ProjectDeleted 76 | ; 77 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/projects/projects.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { DataPersistence } from '@nrwl/nx'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { Project } from '../../projects/project.model'; 7 | import { ProjectsService } from '../../projects/projects.service'; 8 | 9 | import { 10 | AddProject, 11 | DeleteProject, 12 | ProjectAdded, 13 | ProjectDeleted, 14 | ProjectsActionTypes, 15 | ProjectsLoaded, 16 | ProjectUpdated, 17 | LoadProjects, 18 | UpdateProject, 19 | } from './projects.actions'; 20 | import { ProjectsState } from './projects.reducer'; 21 | 22 | @Injectable({providedIn: 'root'}) 23 | export class ProjectsEffects { 24 | @Effect() effect$ = this.actions$.pipe(ofType(ProjectsActionTypes.ProjectsAction)); 25 | 26 | @Effect() 27 | loadProjects$ = this.dataPersistence.fetch(ProjectsActionTypes.LoadProjects, { 28 | run: (action: LoadProjects, state: ProjectsState) => { 29 | return this.projectsService.all().pipe(map((res: Project[]) => new ProjectsLoaded(res))) 30 | }, 31 | 32 | onError: (action: LoadProjects, error) => { 33 | console.error('Error', error); 34 | } 35 | }); 36 | 37 | @Effect() 38 | addProject$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.AddProject, { 39 | run: (action: AddProject, state: ProjectsState) => { 40 | return this.projectsService.create(action.payload).pipe(map((res: Project) => new ProjectAdded(res))) 41 | }, 42 | 43 | onError: (action: AddProject, error) => { 44 | console.error('Error', error); 45 | } 46 | }); 47 | 48 | @Effect() 49 | updateProject$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.UpdateProject, { 50 | run: (action: UpdateProject, state: ProjectsState) => { 51 | return this.projectsService.update(action.payload).pipe(map((res: Project) => new ProjectUpdated(res))) 52 | }, 53 | 54 | onError: (action: UpdateProject, error) => { 55 | console.error('Error', error); 56 | } 57 | }); 58 | 59 | @Effect() 60 | deleteProject$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.DeleteProject, { 61 | run: (action: DeleteProject, state: ProjectsState) => { 62 | return this.projectsService.delete(action.payload).pipe(map(_ => new ProjectDeleted(action.payload))) 63 | }, 64 | 65 | onError: (action: DeleteProject, error) => { 66 | console.error('Error', error); 67 | } 68 | }); 69 | 70 | constructor( 71 | private actions$: Actions, 72 | private dataPersistence: DataPersistence, 73 | private projectsService: ProjectsService 74 | ) {} 75 | } 76 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/projects/projects.facade.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActionsSubject, select, Store } from '@ngrx/store'; 3 | import { filter } from 'rxjs/operators'; 4 | 5 | import { selectAllProjects, selectCurrentProject } from '..'; 6 | import { ProjectsActionTypes } from './projects.actions'; 7 | import * as ProjectsActions from './projects.actions'; 8 | import { ProjectsState } from './projects.reducer'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class ProjectsFacade { 14 | allProjects$ = this.store.pipe(select(selectAllProjects)); 15 | currentProject$ = this.store.pipe(select(selectCurrentProject)); 16 | 17 | mutations$ = this.actions$ 18 | .pipe( 19 | filter(action => 20 | action.type === ProjectsActionTypes.AddProject 21 | || action.type === ProjectsActionTypes.UpdateProject 22 | || action.type === ProjectsActionTypes.DeleteProject 23 | ) 24 | ); 25 | 26 | constructor(private store: Store, private actions$: ActionsSubject) {} 27 | 28 | selectProject(itemId) { 29 | this.store.dispatch(new ProjectsActions.ProjectSelected(itemId)); 30 | } 31 | 32 | loadProjects() { 33 | this.store.dispatch(new ProjectsActions.LoadProjects()); 34 | } 35 | 36 | addProject(item) { 37 | this.store.dispatch(new ProjectsActions.AddProject(item)); 38 | } 39 | 40 | updateProject(item) { 41 | this.store.dispatch(new ProjectsActions.UpdateProject(item)); 42 | } 43 | 44 | deleteProject(item) { 45 | this.store.dispatch(new ProjectsActions.DeleteProject(item)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/projects/projects.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; 2 | import { Project } from '@workshop/core-data'; 3 | 4 | import { ProjectsActions, ProjectsActionTypes } from './projects.actions'; 5 | 6 | /** 7 | * Interface to the part of the Store containing ProjectsState 8 | * and other information related to ProjectsData. 9 | */ 10 | export interface ProjectsState extends EntityState { 11 | selectedProjectId: string | null; 12 | } 13 | 14 | export const adapter: EntityAdapter = createEntityAdapter(); 15 | export const initialState: ProjectsState = adapter.getInitialState({ 16 | // additional entity state properties 17 | selectedProjectId: null, 18 | }); 19 | 20 | export function projectsReducer(state = initialState, action: ProjectsActions): ProjectsState { 21 | switch (action.type) { 22 | case ProjectsActionTypes.ProjectSelected: { 23 | return Object.assign({}, state, { selectedProjectId: action.payload }); 24 | } 25 | 26 | case ProjectsActionTypes.ProjectsLoaded: { 27 | return adapter.addAll(action.payload, state); 28 | } 29 | 30 | case ProjectsActionTypes.ProjectAdded: { 31 | return adapter.addOne(action.payload, state); 32 | } 33 | 34 | case ProjectsActionTypes.ProjectUpdated: { 35 | return adapter.upsertOne(action.payload, state); 36 | } 37 | 38 | case ProjectsActionTypes.ProjectDeleted: { 39 | return adapter.removeOne(action.payload.id, state); 40 | } 41 | 42 | default: 43 | return state; 44 | } 45 | } 46 | 47 | export const getSelectedProjectId = (state: ProjectsState) => state.selectedProjectId; 48 | 49 | // get the selectors 50 | const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); 51 | 52 | // select the array of project ids 53 | export const selectProjectIds = selectIds; 54 | 55 | // select the dictionary of project entities 56 | export const selectProjectEntities = selectEntities; 57 | 58 | // select the array of projects 59 | export const selectAllProjects = selectAll; 60 | 61 | // select the total project count 62 | export const selectProjectTotal = selectTotal; 63 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/state.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { StateModule } from './state.module'; 2 | 3 | describe('StateModule', () => { 4 | let stateModule: StateModule; 5 | 6 | beforeEach(() => { 7 | stateModule = new StateModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(stateModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/state/state.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | import { StoreModule } from '@ngrx/store'; 5 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 6 | import { NxModule } from '@nrwl/nx'; 7 | 8 | import { reducers } from '.'; 9 | import { CustomersEffects } from './customers/customers.effects'; 10 | import { ProjectsEffects } from './projects/projects.effects'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | NxModule.forRoot(), 16 | StoreModule.forRoot(reducers), 17 | StoreDevtoolsModule.instrument({ maxAge: 10 }), 18 | EffectsModule.forRoot([ 19 | CustomersEffects, 20 | ProjectsEffects 21 | ]), 22 | ], 23 | declarations: [] 24 | }) 25 | export class StateModule { } 26 | -------------------------------------------------------------------------------- /libs/core-data/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /libs/core-data/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/core-data", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true 27 | }, 28 | "exclude": [ 29 | "src/test.ts", 30 | "**/*.spec.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /libs/core-data/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/core-data", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/core-data/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/material/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 | const { join } = require('path'); 5 | const getBaseKarmaConfig = require('../../karma.conf'); 6 | 7 | module.exports = function(config) { 8 | const baseConfig = getBaseKarmaConfig(); 9 | config.set({ 10 | ...baseConfig, 11 | coverageIstanbulReporter: { 12 | ...baseConfig.coverageIstanbulReporter, 13 | dir: join(__dirname, '../../coverage/libs/material') 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /libs/material/src/index.ts: -------------------------------------------------------------------------------- 1 | export { MaterialModule } from './lib/material.module'; 2 | -------------------------------------------------------------------------------- /libs/material/src/lib/material.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { MaterialModule } from './material.module'; 3 | 4 | describe('MaterialModule', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [MaterialModule] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create', () => { 12 | expect(MaterialModule).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /libs/material/src/lib/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | MatButtonModule, 4 | MatCardModule, 5 | MatCheckboxModule, 6 | MatFormFieldModule, 7 | MatGridListModule, 8 | MatIconModule, 9 | MatInputModule, 10 | MatListModule, 11 | MatMenuModule, 12 | MatSelectModule, 13 | MatSidenavModule, 14 | MatSliderModule, 15 | MatSnackBarModule, 16 | MatTableModule, 17 | MatToolbarModule, 18 | MatButtonToggleModule 19 | } from '@angular/material'; 20 | 21 | @NgModule({ 22 | imports: [ 23 | MatButtonModule, 24 | MatCardModule, 25 | MatCheckboxModule, 26 | MatFormFieldModule, 27 | MatGridListModule, 28 | MatIconModule, 29 | MatInputModule, 30 | MatListModule, 31 | MatMenuModule, 32 | MatSelectModule, 33 | MatSidenavModule, 34 | MatSliderModule, 35 | MatSnackBarModule, 36 | MatTableModule, 37 | MatToolbarModule, 38 | MatButtonToggleModule 39 | ], 40 | exports: [ 41 | MatButtonModule, 42 | MatCardModule, 43 | MatCheckboxModule, 44 | MatFormFieldModule, 45 | MatGridListModule, 46 | MatIconModule, 47 | MatInputModule, 48 | MatListModule, 49 | MatMenuModule, 50 | MatSelectModule, 51 | MatSidenavModule, 52 | MatSliderModule, 53 | MatSnackBarModule, 54 | MatTableModule, 55 | MatToolbarModule, 56 | MatButtonToggleModule 57 | ] 58 | }) 59 | export class MaterialModule {} 60 | -------------------------------------------------------------------------------- /libs/material/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /libs/material/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/material", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true 27 | }, 28 | "exclude": [ 29 | "src/test.ts", 30 | "**/*.spec.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /libs/material/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/material", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/material/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "workshop", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "workshop", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/ui-login/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 | const { join } = require('path'); 5 | const getBaseKarmaConfig = require('../../karma.conf'); 6 | 7 | module.exports = function(config) { 8 | const baseConfig = getBaseKarmaConfig(); 9 | config.set({ 10 | ...baseConfig, 11 | coverageIstanbulReporter: { 12 | ...baseConfig.coverageIstanbulReporter, 13 | dir: join(__dirname, '../../coverage/libs/ui-login') 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /libs/ui-login/src/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/libs/ui-login/src/assets/background.jpg -------------------------------------------------------------------------------- /libs/ui-login/src/index.ts: -------------------------------------------------------------------------------- 1 | export { UiLoginModule } from './lib/ui-login.module'; 2 | export { LoginComponent } from './lib/login/login.component'; 3 | -------------------------------------------------------------------------------- /libs/ui-login/src/lib/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Login 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /libs/ui-login/src/lib/login/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/libs/ui-login/src/lib/login/login.component.scss -------------------------------------------------------------------------------- /libs/ui-login/src/lib/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [LoginComponent] 12 | }).compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(LoginComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/ui-login/src/lib/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from '@workshop/core-data'; 4 | 5 | @Component({ 6 | selector: 'ui-login', 7 | templateUrl: './login.component.html', 8 | styleUrls: ['./login.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class LoginComponent implements OnInit { 12 | filters = [ 13 | 'ig-xpro2', 14 | 'ig-willow', 15 | 'ig-walden', 16 | 'ig-valencia', 17 | 'ig-toaster', 18 | 'ig-sutro', 19 | 'ig-sierra', 20 | 'ig-rise', 21 | 'ig-nashville', 22 | 'ig-mayfair', 23 | 'ig-lofi', 24 | 'ig-kelvin', 25 | 'ig-inkwell', 26 | 'ig-hudson', 27 | 'ig-hefe', 28 | 'ig-earlybird', 29 | 'ig-brannan', 30 | 'ig-amaro', 31 | 'ig-1977' 32 | ]; 33 | 34 | chosenFilter = this.filters[Math.floor(Math.random() * this.filters.length)]; 35 | userLogin = { email: '', password: ''}; 36 | 37 | constructor(private router: Router, private authService: AuthService) { } 38 | 39 | ngOnInit() { } 40 | 41 | login(email, password) { 42 | // Store the token 43 | this.authService.setToken('you_are_golden'); 44 | // Redirect to home 45 | this.router.navigate(['']); 46 | 47 | // If we really wanted to log in... 48 | // this.authService.login(email, password) 49 | // .subscribe(result => { 50 | // // Store the token 51 | // this.authService.setToken(result['access_token']); 52 | // // Redirect to home 53 | // this.router.navigate(['']); 54 | // }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /libs/ui-login/src/lib/ui-login.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { UiLoginModule } from './ui-login.module'; 3 | 4 | describe('UiLoginModule', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [UiLoginModule] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create', () => { 12 | expect(UiLoginModule).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /libs/ui-login/src/lib/ui-login.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Route, RouterModule } from '@angular/router'; 5 | import { MaterialModule } from '@workshop/material'; 6 | 7 | import { LoginComponent } from './login/login.component'; 8 | 9 | export const uiLoginRoutes: Route[] = []; 10 | 11 | @NgModule({ 12 | imports: [CommonModule, FormsModule, MaterialModule, RouterModule], 13 | declarations: [LoginComponent], 14 | entryComponents: [LoginComponent], 15 | exports: [LoginComponent] 16 | }) 17 | export class UiLoginModule {} 18 | -------------------------------------------------------------------------------- /libs/ui-login/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /libs/ui-login/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/ui-login", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true 27 | }, 28 | "exclude": [ 29 | "src/test.ts", 30 | "**/*.spec.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /libs/ui-login/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/ui-login", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/ui-login/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "ui", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "ui", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/ui-toolbar/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 | const { join } = require('path'); 5 | const getBaseKarmaConfig = require('../../karma.conf'); 6 | 7 | module.exports = function(config) { 8 | const baseConfig = getBaseKarmaConfig(); 9 | config.set({ 10 | ...baseConfig, 11 | coverageIstanbulReporter: { 12 | ...baseConfig.coverageIstanbulReporter, 13 | dir: join(__dirname, '../../coverage/libs/ui-toolbar') 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/index.ts: -------------------------------------------------------------------------------- 1 | export { UiToolbarModule } from './lib/ui-toolbar.module'; 2 | export { ToolbarComponent } from './lib/toolbar/toolbar.component'; 3 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | menu 5 | 6 | 7 | 8 | 9 | 10 | {{title}} 11 | 12 | 13 | 14 | 15 | person 16 | 17 | 18 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/libs/ui-toolbar/src/lib/toolbar/toolbar.component.scss -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToolbarComponent } from './toolbar.component'; 4 | 5 | describe('ToolbarComponent', () => { 6 | let component: ToolbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ToolbarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ToolbarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { Component, OnInit, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core'; 3 | import { AuthService } from '@workshop/core-data'; 4 | 5 | @Component({ 6 | selector: 'ui-toolbar', 7 | templateUrl: './toolbar.component.html', 8 | styleUrls: ['./toolbar.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ToolbarComponent { 12 | @Input() isLoggedIn; 13 | @Input() title; 14 | @Input() sidenav; 15 | @Output() logout = new EventEmitter(); 16 | } 17 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/ui-toolbar.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { UiToolbarModule } from './ui-toolbar.module'; 3 | 4 | describe('UiToolbarModule', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [UiToolbarModule] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create', () => { 12 | expect(UiToolbarModule).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/ui-toolbar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MaterialModule } from '@workshop/material'; 4 | 5 | import { ToolbarComponent } from './toolbar/toolbar.component'; 6 | @NgModule({ 7 | imports: [ 8 | CommonModule, 9 | MaterialModule 10 | ], 11 | declarations: [ToolbarComponent], 12 | exports: [ToolbarComponent] 13 | }) 14 | export class UiToolbarModule {} 15 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /libs/ui-toolbar/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/ui-toolbar", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true 27 | }, 28 | "exclude": [ 29 | "src/test.ts", 30 | "**/*.spec.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /libs/ui-toolbar/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc/libs/ui-toolbar", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/ui-toolbar/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "ui", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "ui", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "workshop", 3 | "implicitDependencies": { 4 | "angular.json": "*", 5 | "package.json": "*", 6 | "tsconfig.json": "*", 7 | "tslint.json": "*", 8 | "nx.json": "*" 9 | }, 10 | "projects": { 11 | "core-data": { 12 | "tags": [] 13 | }, 14 | "dashboard": { 15 | "tags": [] 16 | }, 17 | "dashboard-e2e": { 18 | "tags": [] 19 | }, 20 | "ui-login": { 21 | "tags": [] 22 | }, 23 | "material": { 24 | "tags": [] 25 | }, 26 | "ui-toolbar": { 27 | "tags": [] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-core-workshop", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "concurrently \"npm run server\" \"ng serve\"", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "./node_modules/.bin/nx lint && ng lint", 11 | "e2e": "ng e2e", 12 | "server": "json-server server/db.json", 13 | "server:auth": "node server/server.js", 14 | "affected:apps": "./node_modules/.bin/nx affected:apps", 15 | "affected:libs": "./node_modules/.bin/nx affected:libs", 16 | "affected:build": "./node_modules/.bin/nx affected:build", 17 | "affected:e2e": "./node_modules/.bin/nx affected:e2e", 18 | "affected:test": "./node_modules/.bin/nx affected:test", 19 | "affected:lint": "./node_modules/.bin/nx affected:lint", 20 | "affected:dep-graph": "./node_modules/.bin/nx affected:dep-graph", 21 | "format": "./node_modules/.bin/nx format:write", 22 | "format:write": "./node_modules/.bin/nx format:write", 23 | "format:check": "./node_modules/.bin/nx format:check", 24 | "update": "ng update @nrwl/schematics", 25 | "update:check": "ng update", 26 | "workspace-schematic": "./node_modules/.bin/nx workspace-schematic", 27 | "dep-graph": "./node_modules/.bin/nx dep-graph", 28 | "help": "./node_modules/.bin/nx help", 29 | "affected": "./node_modules/.bin/nx affected" 30 | }, 31 | "private": true, 32 | "dependencies": { 33 | "@angular/animations": "^8.2.12", 34 | "@angular/cdk": "^8.2.3", 35 | "@angular/common": "^8.2.12", 36 | "@angular/compiler": "^8.2.12", 37 | "@angular/core": "^8.2.12", 38 | "@angular/forms": "^8.2.12", 39 | "@angular/material": "^8.2.3", 40 | "@angular/material-moment-adapter": "^8.2.3", 41 | "@angular/platform-browser": "^8.2.12", 42 | "@angular/platform-browser-dynamic": "^8.2.12", 43 | "@angular/router": "^8.2.12", 44 | "@auth0/angular-jwt": "^3.0.0", 45 | "@ngrx/effects": "8.4.0", 46 | "@ngrx/router-store": "8.4.0", 47 | "@ngrx/store": "8.4.0", 48 | "@nrwl/nx": "7.8.7", 49 | "core-js": "^3.3.5", 50 | "hammerjs": "^2.0.8", 51 | "jsonwebtoken": "^8.3.0", 52 | "moment": "^2.23.0", 53 | "rxjs": "^6.5.3", 54 | "tslib": "^1.9.0", 55 | "zone.js": "^0.9.1", 56 | "@ngrx/entity": "8.4.0" 57 | }, 58 | "devDependencies": { 59 | "@angular-devkit/build-angular": "0.803.21", 60 | "@angular/cli": "8.3.15", 61 | "@angular/compiler-cli": "^8.2.12", 62 | "@angular/language-service": "^8.2.12", 63 | "@ngrx/store-devtools": "8.4.0", 64 | "@nrwl/schematics": "8.7.0", 65 | "@types/jasmine": "~2.8.6", 66 | "@types/jasminewd2": "~2.0.3", 67 | "@types/node": "~8.9.4", 68 | "codelyzer": "~4.5.0", 69 | "concurrently": "^4.0.1", 70 | "jasmine-core": "~2.99.1", 71 | "jasmine-marbles": "0.4.0", 72 | "jasmine-spec-reporter": "~4.2.1", 73 | "json-server": "^0.14.0", 74 | "karma": "^4.2.0", 75 | "karma-chrome-launcher": "~2.2.0", 76 | "karma-coverage-istanbul-reporter": "2.1.1", 77 | "karma-jasmine": "~1.1.0", 78 | "karma-jasmine-html-reporter": "^0.2.2", 79 | "prettier": "1.15.2", 80 | "protractor": "~5.4.0", 81 | "ts-node": "~7.0.0", 82 | "tslint": "~5.11.0", 83 | "typescript": "~3.5.3" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "customers": [ 3 | { 4 | "id": "1", 5 | "firstName": "Jonathan", 6 | "lastName": "Garvey", 7 | "email": "jon@gmail.coms", 8 | "phone": "1234567890", 9 | "title": "Technical Lead", 10 | "status": 1 11 | }, 12 | { 13 | "id": "2", 14 | "firstName": "Bob", 15 | "lastName": "Jones", 16 | "email": "bob@bobjones.com", 17 | "phone": "9372932391", 18 | "title": "CEO", 19 | "status": 2 20 | }, 21 | { 22 | "id": "3", 23 | "firstName": "Lukas", 24 | "lastName": "Ruebbelke", 25 | "email": "hey.player@gmail.com", 26 | "phone": "9876543212", 27 | "title": "CEO", 28 | "status": 1 29 | }, 30 | { 31 | "id": "4", 32 | "firstName": "Sally", 33 | "lastName": "McBride", 34 | "email": "silly.sally@hotmail.com", 35 | "phone": "3252352531", 36 | "title": "Receptionist", 37 | "status": 3 38 | }, 39 | { 40 | "id": "5", 41 | "firstName": "Donna", 42 | "lastName": "Barkley", 43 | "email": "donnab@aol.com", 44 | "phone": "0192837475", 45 | "title": "Student", 46 | "status": 4 47 | }, 48 | { 49 | "id": "6", 50 | "firstName": "Larry", 51 | "lastName": "Fitzgerald", 52 | "email": "super.secret@gmail.com", 53 | "phone": "1234432122", 54 | "title": "Quarterback", 55 | "status": 4 56 | }, 57 | { 58 | "id": "7", 59 | "firstName": "Victor", 60 | "lastName": "Wooten", 61 | "email": "w00t@victor.com", 62 | "phone": "1888333992", 63 | "title": "Bassist", 64 | "status": 1 65 | }, 66 | { 67 | "id": "8", 68 | "firstName": "Chris", 69 | "lastName": "Take", 70 | "email": "crispy@gmail.com", 71 | "phone": "9927381924", 72 | "title": "Developer", 73 | "status": 1 74 | }, 75 | { 76 | "id": "9", 77 | "firstName": "Christian", 78 | "lastName": "Peterson", 79 | "email": "cp3@lol.com", 80 | "phone": "0099831234", 81 | "title": "Handyman", 82 | "status": 3 83 | }, 84 | { 85 | "id": "10", 86 | "firstName": "RIP", 87 | "lastName": "Stick", 88 | "email": "r.i.p@deadly.com", 89 | "phone": "6123512311", 90 | "title": "Grim Reaper", 91 | "status": 2 92 | } 93 | ], 94 | "projects": [ 95 | { 96 | "id": "1", 97 | "title": "Project One", 98 | "details": "This is a sample project", 99 | "percentComplete": 20, 100 | "approved": false, 101 | "startDate": null, 102 | "targetDate": null, 103 | "completionDate": null, 104 | "customerId": "1" 105 | }, 106 | { 107 | "id": "2", 108 | "title": "Project Two", 109 | "details": "This is a sample project", 110 | "percentComplete": 40, 111 | "approved": false, 112 | "startDate": null, 113 | "targetDate": null, 114 | "completionDate": null, 115 | "customerId": "3" 116 | }, 117 | { 118 | "id": "3", 119 | "title": "Project Three", 120 | "details": "This is a sample project", 121 | "percentComplete": 60, 122 | "approved": false, 123 | "startDate": null, 124 | "targetDate": null, 125 | "completionDate": null, 126 | "customerId": "7" 127 | }, 128 | { 129 | "id": "5", 130 | "title": "Project Five", 131 | "details": "This is a sample project", 132 | "percentComplete": 100, 133 | "approved": true, 134 | "startDate": null, 135 | "targetDate": "2018-10-26T07:00:00.000Z", 136 | "completionDate": null, 137 | "customerId": "1" 138 | } 139 | ] 140 | } -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const jsonServer = require('json-server') 3 | const jwt = require('jsonwebtoken') 4 | const middlewares = jsonServer.defaults() 5 | 6 | const server = jsonServer.create() 7 | const router = jsonServer.router('./server/db.json') 8 | const userdb = JSON.parse(fs.readFileSync('./server/users.json', 'UTF-8')) 9 | 10 | server.use(middlewares); 11 | 12 | const SECRET_KEY = '123456789' 13 | const expiresIn = '1h' 14 | 15 | // Create a token from a payload 16 | function createToken(payload) { 17 | return jwt.sign(payload, SECRET_KEY, { expiresIn }) 18 | } 19 | 20 | // Verify the token 21 | function verifyToken(token) { 22 | return jwt.verify(token, SECRET_KEY, (err, decode) => decode !== undefined ? decode : err) 23 | } 24 | 25 | // Check if the user exists in database 26 | function isAuthenticated({ email, password }) { 27 | return userdb.users.findIndex(user => user.email === email && user.password === password) !== -1 28 | } 29 | 30 | // To handle POST, PUT and PATCH you need to use a body-parser 31 | // You can use the one used by JSON Server 32 | server.use(jsonServer.bodyParser) 33 | server.post('/auth/login', (req, res) => { 34 | const {email, password} = req.body 35 | if (!isAuthenticated({email, password})) { 36 | const status = 401 37 | const message = 'Incorrect email or password' 38 | res.status(status).json({status, message}) 39 | return 40 | } 41 | const access_token = createToken({email, password}) 42 | res.status(200).json({access_token}) 43 | }) 44 | 45 | server.use(/^(?!\/auth).*$/, (req, res, next) => { 46 | if (req.headers.authorization === undefined || req.headers.authorization.split(' ')[0] !== 'Bearer') { 47 | const status = 401 48 | const message = 'Error in authorization format' 49 | res.status(status).json({ status, message }) 50 | return 51 | } 52 | try { 53 | verifyToken(req.headers.authorization.split(' ')[1]) 54 | next() 55 | } catch (err) { 56 | const status = 401 57 | const message = 'Error access_token is revoked' 58 | res.status(status).json({ status, message }) 59 | } 60 | }) 61 | 62 | server.use(router) 63 | 64 | server.listen(3000, () => { 65 | console.log('Run Auth API Server') 66 | }) 67 | -------------------------------------------------------------------------------- /server/users.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "users": [ 4 | { 5 | "id": 1, 6 | "name": "admin", 7 | "email": "admin@email.com", 8 | "password": "admin" 9 | }, 10 | { 11 | "id": 2, 12 | "name": "techie", 13 | "email": "techie@email.com", 14 | "password": "techie" 15 | }, 16 | { 17 | "id": 3, 18 | "name": "user", 19 | "email": "user@email.com", 20 | "password": "user" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/angular-reactive-workshop/bdf59ceb5b1e3627e89c44544ff4621d3c868a4b/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "include": [ 14 | "**/*.ts" 15 | ] 16 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ], 18 | "baseUrl": ".", 19 | "paths": { 20 | "@env/*": [ 21 | "apps/dashboard/src/environments/*" 22 | ], 23 | "@workshop/core-data": [ 24 | "libs/core-data/src/index.ts" 25 | ], 26 | "@workshop/material": [ 27 | "libs/material/src/index.ts" 28 | ], 29 | "@workshop/ui-login": [ 30 | "libs/ui-login/src/index.ts" 31 | ], 32 | "@workshop/ui-toolbar": [ 33 | "libs/ui-toolbar/src/index.ts" 34 | ] 35 | } 36 | }, 37 | "exclude": [ 38 | "node_modules", 39 | "tmp" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer", 4 | "node_modules/@nrwl/schematics/src/tslint" 5 | ], 6 | "rules": { 7 | "arrow-return-shorthand": true, 8 | "callable-types": true, 9 | "class-name": true, 10 | "deprecation": { 11 | "severity": "warn" 12 | }, 13 | "forin": true, 14 | "import-blacklist": [ 15 | true, 16 | "rxjs/Rx" 17 | ], 18 | "interface-over-type-literal": true, 19 | "member-access": false, 20 | "member-ordering": [ 21 | true, 22 | { 23 | "order": [ 24 | "static-field", 25 | "instance-field", 26 | "static-method", 27 | "instance-method" 28 | ] 29 | } 30 | ], 31 | "no-arg": true, 32 | "no-bitwise": true, 33 | "no-console": [ 34 | true, 35 | "debug", 36 | "info", 37 | "time", 38 | "timeEnd", 39 | "trace" 40 | ], 41 | "no-construct": true, 42 | "no-debugger": true, 43 | "no-duplicate-super": true, 44 | "no-empty": false, 45 | "no-empty-interface": true, 46 | "no-eval": true, 47 | "no-inferrable-types": [ 48 | true, 49 | "ignore-params" 50 | ], 51 | "no-misused-new": true, 52 | "no-non-null-assertion": true, 53 | "no-shadowed-variable": true, 54 | "no-string-literal": false, 55 | "no-string-throw": true, 56 | "no-switch-case-fall-through": true, 57 | "no-unnecessary-initializer": true, 58 | "no-unused-expression": true, 59 | "no-use-before-declare": true, 60 | "no-var-keyword": true, 61 | "object-literal-sort-keys": false, 62 | "prefer-const": true, 63 | "radix": true, 64 | "triple-equals": [ 65 | true, 66 | "allow-null-check" 67 | ], 68 | "unified-signatures": true, 69 | "variable-name": false, 70 | "directive-selector": [ 71 | true, 72 | "attribute", 73 | "app", 74 | "camelCase" 75 | ], 76 | "component-selector": [ 77 | true, 78 | "element", 79 | "app", 80 | "kebab-case" 81 | ], 82 | "no-output-on-prefix": true, 83 | "use-input-property-decorator": true, 84 | "use-output-property-decorator": true, 85 | "use-host-property-decorator": true, 86 | "no-input-rename": true, 87 | "no-output-rename": true, 88 | "use-life-cycle-interface": true, 89 | "use-pipe-transform-interface": true, 90 | "component-class-suffix": true, 91 | "directive-class-suffix": true, 92 | 93 | "nx-enforce-module-boundaries": [ 94 | true, 95 | { 96 | "allow": [], 97 | "depConstraints": [ 98 | { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } 99 | ] 100 | } 101 | ] 102 | } 103 | } 104 | --------------------------------------------------------------------------------
12 | {{project.details}} 13 |