├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── README.md ├── angular.json ├── docker-compose.debug.yml ├── docker-compose.yml ├── karma.conf.js ├── nginx.conf ├── package-lock.json ├── package.json ├── src ├── api │ ├── in-memory-store.service.js │ ├── in-memory-store.service.js.map │ ├── in-memory-store.service.ts │ ├── sessions.json │ └── speakers.json ├── app │ ├── admin │ │ ├── admin-routing.module.ts │ │ ├── admin.component.ts │ │ └── admin.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── config.ts │ │ ├── entity.service.ts │ │ ├── exception.service.ts │ │ ├── guards │ │ │ ├── auth-load.guard.ts │ │ │ ├── auth.guard.ts │ │ │ └── can-deactivate.guard.ts │ │ ├── index.ts │ │ ├── interceptors │ │ │ ├── auth.interceptor.ts │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── csrf.interceptor.ts │ │ │ ├── ensure-ssl.interceptor.ts │ │ │ ├── index.ts │ │ │ ├── log-headers.interceptor.ts │ │ │ ├── log-response.interceptor.ts │ │ │ └── transform-response.interceptor.ts │ │ ├── message.service.ts │ │ ├── modal.component.ts │ │ ├── modal.service.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── speaker.model.ts │ │ │ └── speaker.service.ts │ │ ├── module-import-check.ts │ │ ├── nav.component.ts │ │ ├── page-not-found.component.ts │ │ ├── spinner.component.ts │ │ ├── spinner.service.ts │ │ ├── strategies │ │ │ ├── index.ts │ │ │ ├── network-aware-preload-strategy.ts │ │ │ ├── on-demand-preload-strategy.ts │ │ │ ├── on-demand-preload.service.ts │ │ │ └── opt-in-preload-strategy.ts │ │ ├── toast.component.ts │ │ ├── toast.service.ts │ │ └── user-profile.service.ts │ ├── dashboard │ │ ├── dashboard-routing.module.ts │ │ ├── dashboard.component.css │ │ ├── dashboard.component.html │ │ ├── dashboard.component.ts │ │ ├── dashboard.module.ts │ │ └── shared │ │ │ └── dashboard-button │ │ │ ├── dashboard-button.component.css │ │ │ ├── dashboard-button.component.html │ │ │ └── dashboard-button.component.ts │ ├── login │ │ ├── login-routing.module.ts │ │ ├── login.component.ts │ │ ├── login.module.ts │ │ └── login.service.ts │ ├── routes.ts │ ├── sessions │ │ ├── session-list │ │ │ ├── session-list.component.css │ │ │ ├── session-list.component.html │ │ │ └── session-list.component.ts │ │ ├── session │ │ │ ├── session.component.css │ │ │ ├── session.component.html │ │ │ └── session.component.ts │ │ ├── sessions-routing.module.ts │ │ ├── sessions.component.ts │ │ ├── sessions.module.ts │ │ └── shared │ │ │ ├── session-button │ │ │ ├── session-button.component.css │ │ │ ├── session-button.component.html │ │ │ └── session-button.component.ts │ │ │ ├── session-resolver.service.ts │ │ │ ├── session.model.ts │ │ │ └── session.service.ts │ ├── shared │ │ ├── filter-text │ │ │ ├── filter-text.component.css │ │ │ ├── filter-text.component.html │ │ │ ├── filter-text.component.ts │ │ │ ├── filter-text.module.ts │ │ │ └── filter-text.service.ts │ │ ├── init-caps.pipe.ts │ │ └── shared.module.ts │ └── speakers │ │ ├── shared │ │ └── speaker-button │ │ │ ├── speaker-button.component.css │ │ │ ├── speaker-button.component.html │ │ │ └── speaker-button.component.ts │ │ ├── speaker-list │ │ ├── speaker-list.component.css │ │ ├── speaker-list.component.html │ │ └── speaker-list.component.ts │ │ ├── speaker │ │ ├── speaker.component.css │ │ ├── speaker.component.html │ │ └── speaker.component.ts │ │ ├── speakers-routing.module.ts │ │ ├── speakers.component.ts │ │ └── speakers.module.ts ├── assets │ ├── .gitkeep │ ├── animate.css │ ├── app.css │ ├── material.min.css │ ├── material.min.js │ ├── material.min.js.map │ ├── ng.png │ ├── sprite-av-white.css │ └── sprite-av-white.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts └── typings.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | README.md 9 | LICENSE 10 | .vscode -------------------------------------------------------------------------------- /.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 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | .angular/cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Angular", 6 | "type": "chrome", 7 | "request": "launch", 8 | "preLaunchTask": "npm: start-ng", 9 | "url": "http://localhost:4200/", 10 | "webRoot": "${workspaceFolder}" 11 | }, 12 | { 13 | "name": "Attach to Chrome", 14 | "type": "chrome", 15 | "request": "attach", 16 | "port": 9222, 17 | "sourceMaps": true, 18 | "trace": true, 19 | "webRoot": "${workspaceRoot}", 20 | "url": "http://localhost:4200/*", 21 | "sourceMapPathOverrides": { 22 | "webpack:///./*": "${webRoot}/*" 23 | } 24 | }, 25 | { 26 | "name": "Attach to Chrome Test", 27 | "type": "chrome", 28 | "request": "attach", 29 | "port": 9222, 30 | "sourceMaps": true, 31 | "trace": true, 32 | "webRoot": "${workspaceRoot}", 33 | "url": "http://localhost:9876/*", 34 | "sourceMapPathOverrides": { 35 | "webpack:///./*": "${webRoot}/*" 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "peacock.cacheBust": "736791", 3 | "peacock.color": "#49b4a8", 4 | "workbench.colorCustomizations": { 5 | "activityBar.background": "#49b4a8", 6 | "activityBar.activeBackground": "#49b4a8", 7 | "activityBar.activeBorder": "#f4e8f6", 8 | "activityBar.foreground": "#15202b", 9 | "activityBar.inactiveForeground": "#15202b99", 10 | "activityBarBadge.background": "#f4e8f6", 11 | "activityBarBadge.foreground": "#15202b", 12 | "titleBar.activeBackground": "#49b4a8", 13 | "titleBar.inactiveBackground": "#49b4a899", 14 | "titleBar.activeForeground": "#15202b", 15 | "titleBar.inactiveForeground": "#15202b99", 16 | "statusBar.background": "#49b4a8", 17 | "statusBarItem.hoverBackground": "#3a9086", 18 | "statusBar.foreground": "#15202b", 19 | "commandCenter.border": "#15202b99", 20 | "sash.hoverBorder": "#49b4a8", 21 | "statusBarItem.remoteBackground": "#49b4a8", 22 | "statusBarItem.remoteForeground": "#15202b" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "start-ng", 9 | "isBackground": true, 10 | "presentation": { 11 | "focus": true, 12 | "panel": "dedicated" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "problemMatcher": { 19 | "owner": "typescript", 20 | "source": "ts", 21 | "applyTo": "closedDocuments", 22 | "fileLocation": ["relative", "${cwd}"], 23 | "pattern": "$tsc", 24 | "background": { 25 | "activeOnStart": true, 26 | "beginsPattern": { 27 | "regexp": "(.*?)" 28 | }, 29 | "endsPattern": { 30 | "regexp": "Compiled |Failed to compile." 31 | } 32 | } 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Angular App ======================================== 2 | FROM johnpapa/angular-cli as angular-app 3 | LABEL authors="John Papa" 4 | # Copy and install the Angular app 5 | RUN mkdir -p /app 6 | WORKDIR /app 7 | COPY . /app 8 | RUN npm install 9 | RUN ng build --prod 10 | 11 | #nginx server ======================================= 12 | FROM nginx:alpine 13 | LABEL authors="John Papa" 14 | RUN mkdir -p /usr/src/app 15 | WORKDIR /usr/src/app 16 | # Copy custom nginx config 17 | COPY ./nginx.conf /etc/nginx/nginx.conf 18 | COPY --from=angular-app /app/dist /usr/src/app 19 | EXPOSE 80 443 20 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EventViewCli 2 | 3 | Angular Demo with a Little bit of a lot of features 4 | 5 | ## Development server 6 | 7 | Run `npm run start-ng` for a dev server. Navigate to `http://localhost:4300/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Resources 10 | 11 | - Get [VS Code](https://code.visualstudio.com/?wt.mc_id=angulareventviewcli-github-jopapa) 12 | - Get the VS Code [Angular Essentials](https://marketplace.visualstudio.com/items?itemName=johnpapa.angular-essentials&wt.mc_id=angulareventviewcli-github-jopapa) 13 | - Get the VS Code [Angular Snippets](https://marketplace.visualstudio.com/items?itemName=johnpapa.angular2&wt.mc_id=angulareventviewcli-github-jopapa) 14 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "newProjectRoot": "projects", 4 | "projects": { 5 | "event-view-cli": { 6 | "root": "", 7 | "projectType": "application", 8 | "architect": { 9 | "build": { 10 | "builder": "@angular-devkit/build-angular:browser", 11 | "options": { 12 | "outputPath": "dist", 13 | "index": "src/index.html", 14 | "main": "src/main.ts", 15 | "tsConfig": "tsconfig.app.json", 16 | "polyfills": "src/polyfills.ts", 17 | "assets": [ 18 | { 19 | "glob": "**/*", 20 | "input": "src/assets", 21 | "output": "/assets" 22 | }, 23 | { 24 | "glob": "favicon.ico", 25 | "input": "src", 26 | "output": "/" 27 | }, 28 | { 29 | "glob": "**/*", 30 | "input": "src/api", 31 | "output": "/api" 32 | } 33 | ], 34 | "styles": [ 35 | { 36 | "input": "src/assets/material.min.css", 37 | "inject": true 38 | }, 39 | { 40 | "input": "src/assets/sprite-av-white.css", 41 | "inject": true 42 | }, 43 | { 44 | "input": "src/assets/animate.css", 45 | "inject": true 46 | }, 47 | { 48 | "input": "src/assets/app.css", 49 | "inject": true 50 | }, 51 | { 52 | "input": "src/styles.css", 53 | "inject": true 54 | } 55 | ], 56 | "scripts": [ 57 | { 58 | "input": "src/assets/material.min.js", 59 | "inject": true 60 | } 61 | ], 62 | "vendorChunk": true, 63 | "extractLicenses": false, 64 | "buildOptimizer": false, 65 | "sourceMap": true, 66 | "optimization": false, 67 | "namedChunks": true 68 | }, 69 | "configurations": { 70 | "production": { 71 | "budgets": [ 72 | { 73 | "type": "anyComponentStyle", 74 | "maximumWarning": "6kb" 75 | } 76 | ], 77 | "optimization": true, 78 | "outputHashing": "all", 79 | "sourceMap": false, 80 | "namedChunks": false, 81 | "extractLicenses": true, 82 | "vendorChunk": false, 83 | "buildOptimizer": true, 84 | "fileReplacements": [ 85 | { 86 | "src": "src/environments/environment.ts", 87 | "replaceWith": "src/environments/environment.prod.ts" 88 | } 89 | ] 90 | } 91 | }, 92 | "defaultConfiguration": "" 93 | }, 94 | "serve": { 95 | "builder": "@angular-devkit/build-angular:dev-server", 96 | "options": { 97 | "buildTarget": "event-view-cli:build" 98 | }, 99 | "configurations": { 100 | "production": { 101 | "buildTarget": "event-view-cli:build:production" 102 | } 103 | } 104 | }, 105 | "extract-i18n": { 106 | "builder": "@angular-devkit/build-angular:extract-i18n", 107 | "options": { 108 | "buildTarget": "event-view-cli:build" 109 | } 110 | }, 111 | "test": { 112 | "builder": "@angular-devkit/build-angular:karma", 113 | "options": { 114 | "main": "src/test.ts", 115 | "karmaConfig": "./karma.conf.js", 116 | "polyfills": "src/polyfills.ts", 117 | "tsConfig": "tsconfig.spec.json", 118 | "scripts": [ 119 | { 120 | "input": "src/assets/material.min.js", 121 | "inject": true 122 | } 123 | ], 124 | "styles": [ 125 | { 126 | "input": "src/assets/material.min.css", 127 | "inject": true 128 | }, 129 | { 130 | "input": "src/assets/sprite-av-white.css", 131 | "inject": true 132 | }, 133 | { 134 | "input": "src/assets/animate.css", 135 | "inject": true 136 | }, 137 | { 138 | "input": "src/assets/app.css", 139 | "inject": true 140 | }, 141 | { 142 | "input": "src/styles.css", 143 | "inject": true 144 | } 145 | ], 146 | "assets": [ 147 | { 148 | "glob": "**/*", 149 | "input": "src/assets", 150 | "output": "/assets" 151 | }, 152 | { 153 | "glob": "favicon.ico", 154 | "input": "src", 155 | "output": "/" 156 | }, 157 | { 158 | "glob": "**/*", 159 | "input": "src/api", 160 | "output": "/api" 161 | } 162 | ] 163 | } 164 | }, 165 | "lint": { 166 | "builder": "@angular-devkit/build-angular:tslint", 167 | "options": { 168 | "tsConfig": ["tsconfig.app.json", "tsconfig.spec.json"], 169 | "exclude": ["**/node_modules/**"] 170 | } 171 | } 172 | } 173 | } 174 | }, 175 | "schematics": { 176 | "@schematics/angular:component": { 177 | "prefix": "ev", 178 | "style": "css" 179 | }, 180 | "@schematics/angular:directive": { 181 | "prefix": "ev" 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /docker-compose.debug.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | angular-event-view-cli: 5 | image: johnpapa/angular-event-view-cli:latest 6 | environment: 7 | NODE_ENV: development 8 | build: . 9 | ports: 10 | - 80:80 11 | - 443:443 12 | - 9229:9229 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | angular-event-view-cli: 5 | image: johnpapa/angular-event-view-cli:latest 6 | environment: 7 | NODE_ENV: production 8 | build: . 9 | ports: 10 | - 80:80 11 | - 443:443 12 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | { pattern: './src/test.ts', watched: false } 20 | ], 21 | preprocessors: { 22 | './src/test.ts': ['@angular-devkit/build-angular'] 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | angularCli: { 32 | environment: 'dev' 33 | }, 34 | reporters: config.angularCli && config.angularCli.codeCoverage 35 | ? ['progress', 'coverage-istanbul'] 36 | : ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | ssl_session_cache shared:SSL:10m; 7 | ssl_session_timeout 30m; 8 | 9 | #See http://blog.argteam.com/coding/hardening-node-js-for-production-part-2-using-nginx-to-avoid-node-js-load 10 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=one:8m max_size=3000m inactive=600m; 11 | proxy_temp_path /var/tmp; 12 | include mime.types; 13 | default_type application/octet-stream; 14 | sendfile on; 15 | keepalive_timeout 65; 16 | 17 | gzip on; 18 | gzip_comp_level 6; 19 | gzip_vary on; 20 | gzip_min_length 1000; 21 | gzip_proxied any; 22 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 23 | gzip_buffers 16 8k; 24 | 25 | server { 26 | listen 80; 27 | server_name localhost; 28 | 29 | location / { 30 | root /usr/src/app; 31 | # root /usr/share/nginx/html; 32 | index index.html; 33 | expires -1; 34 | add_header Pragma "no-cache"; 35 | add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; 36 | try_files $uri$args $uri$args/ $uri $uri/ /index.html =404; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-view-cli", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "start-ng": "ng serve -o --port 4200", 10 | "build": "ng build", 11 | "test": "ng test", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e", 14 | "chrome-debug": "/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222", 15 | "docker": "docker-compose up -d --build", 16 | "docker-debug": "docker-compose -f docker-compose.debug.yml up -d --build", 17 | "docker-down": "docker-compose down", 18 | "sme": "./node_modules/.bin/source-map-explorer", 19 | "gen-stats": "ng build --configuration production --stats-json", 20 | "view-stats": "webpack-bundle-analyzer dist/stats.json" 21 | }, 22 | "private": true, 23 | "dependencies": { 24 | "@angular/animations": "17.3.0", 25 | "@angular/common": "17.3.0", 26 | "@angular/compiler": "17.3.0", 27 | "@angular/core": "17.3.0", 28 | "@angular/forms": "17.3.0", 29 | "@angular/platform-browser": "17.3.0", 30 | "@angular/platform-browser-dynamic": "17.3.0", 31 | "@angular/router": "17.3.0", 32 | "angular-in-memory-web-api": "^0.16.0", 33 | "ngx-quicklink": "^0.4.2", 34 | "rxjs": "^6.6.3", 35 | "tslib": "^2.3.0", 36 | "zone.js": "~0.14.4" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "^17.3.0", 40 | "@angular/cli": "^17.3.0", 41 | "@angular/compiler-cli": "17.3.0", 42 | "@types/jasmine": "~4.0.0", 43 | "jasmine-core": "~4.2.0", 44 | "karma": "~6.4.0", 45 | "karma-chrome-launcher": "~3.1.0", 46 | "karma-coverage": "~2.2.0", 47 | "karma-jasmine": "~5.1.0", 48 | "karma-jasmine-html-reporter": "~2.0.0", 49 | "source-map-explorer": "^2.5.2", 50 | "typescript": "~5.4.2", 51 | "webpack-bundle-analyzer": "^4.6.1", 52 | "webpack-visualizer-plugin": "^0.1.11" 53 | } 54 | } -------------------------------------------------------------------------------- /src/api/in-memory-store.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var InMemoryStoreService = (function() { 3 | function InMemoryStoreService() {} 4 | /** 5 | * Creates fresh copy of data each time. 6 | * Safe for consuming service to morph arrays and objects. 7 | */ 8 | InMemoryStoreService.prototype.createDb = function() { 9 | var speakers = [ 10 | { 11 | id: 11, 12 | name: 'Chewbacca', 13 | twitter: '@im_chewy' 14 | }, 15 | { 16 | id: 12, 17 | name: 'Rey', 18 | twitter: '@rey' 19 | }, 20 | { 21 | id: 13, 22 | name: 'Finn (FN2187)', 23 | twitter: '@finn' 24 | }, 25 | { 26 | id: 14, 27 | name: 'Han Solo', 28 | twitter: '@i_know' 29 | }, 30 | { 31 | id: 15, 32 | name: 'Leia Organa', 33 | twitter: '@organa' 34 | }, 35 | { 36 | id: 16, 37 | name: 'Luke Skywalker', 38 | twitter: '@chosen_one_son' 39 | }, 40 | { 41 | id: 17, 42 | name: 'Poe Dameron', 43 | twitter: '@i_am_poe' 44 | }, 45 | { 46 | id: 18, 47 | name: 'Kylo Ren', 48 | twitter: '@daddy_issues' 49 | }, 50 | { 51 | id: 19, 52 | name: 'Supreme Commander Snoke', 53 | twitter: '@snoker' 54 | }, 55 | { 56 | id: 20, 57 | name: 'R2-D2', 58 | twitter: '@r2d2' 59 | }, 60 | { 61 | id: 21, 62 | name: 'BB8', 63 | twitter: '@bb_eight' 64 | }, 65 | { 66 | id: 22, 67 | name: 'C-3PO', 68 | twitter: '@goldy' 69 | }, 70 | { 71 | id: 23, 72 | name: 'Maz Kanata', 73 | twitter: '@mazzzy' 74 | }, 75 | { 76 | id: 24, 77 | name: 'Captain Phasma', 78 | twitter: '@fazma' 79 | }, 80 | { 81 | id: 25, 82 | name: 'General Hux', 83 | twitter: '@hux' 84 | }, 85 | { 86 | id: 26, 87 | name: 'Lor San Tekka', 88 | twitter: '@lor_san' 89 | } 90 | ]; 91 | var sessions = [ 92 | { 93 | id: 130, 94 | name: 'Angular 2 First Look', 95 | level: 'beginner' 96 | }, 97 | { 98 | id: 132, 99 | name: 'RxJS', 100 | level: 'beginner' 101 | }, 102 | { 103 | id: 133, 104 | name: 'Angular Material', 105 | level: 'beginner' 106 | }, 107 | { 108 | id: 134, 109 | name: 'Redux', 110 | level: 'beginner' 111 | }, 112 | { 113 | id: 135, 114 | name: 'React', 115 | level: 'beginner' 116 | }, 117 | { 118 | id: 136, 119 | name: 'TypeScript', 120 | level: 'beginner' 121 | }, 122 | { 123 | id: 137, 124 | name: 'ES2015', 125 | level: 'beginner' 126 | }, 127 | { 128 | id: 138, 129 | name: 'Mongo', 130 | level: 'beginner' 131 | }, 132 | { 133 | id: 139, 134 | name: 'Redis', 135 | level: 'beginner' 136 | }, 137 | { 138 | id: 140, 139 | name: 'Node', 140 | level: 'beginner' 141 | }, 142 | { 143 | id: 141, 144 | name: 'Express', 145 | level: 'beginner' 146 | } 147 | ]; 148 | var rooms = [ 149 | { 150 | id: 30, 151 | name: 'Millennium Falcon', 152 | type: 'space' 153 | }, 154 | { 155 | id: 32, 156 | name: 'X-Wing Fighter', 157 | type: 'space' 158 | }, 159 | { 160 | id: 33, 161 | name: 'Imperial Star Destroyer', 162 | type: 'space' 163 | }, 164 | { 165 | id: 34, 166 | name: 'AT-AT Walker', 167 | type: 'land' 168 | }, 169 | { 170 | id: 35, 171 | name: 'TIE Fighter', 172 | type: 'space' 173 | }, 174 | { 175 | id: 36, 176 | name: 'B-Wing Fighter', 177 | type: 'space' 178 | }, 179 | { 180 | id: 37, 181 | name: 'ETA-2 Jedi Starfighter', 182 | type: 'space' 183 | }, 184 | { 185 | id: 38, 186 | name: 'TIE Interceptor', 187 | type: 'space' 188 | }, 189 | { 190 | id: 39, 191 | name: 'X-34 Landspeeder', 192 | type: 'land' 193 | }, 194 | { 195 | id: 40, 196 | name: 'Snow Speeder', 197 | type: 'land' 198 | }, 199 | { 200 | id: 41, 201 | name: 'X-34 Landspeeder', 202 | type: 'land' 203 | } 204 | ]; 205 | return { rooms: rooms, speakers: speakers, sessions: sessions }; 206 | }; 207 | return InMemoryStoreService; 208 | })(); 209 | exports.InMemoryStoreService = InMemoryStoreService; 210 | //# sourceMappingURL=in-memory-store.service.js.map 211 | -------------------------------------------------------------------------------- /src/api/in-memory-store.service.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"in-memory-store.service.js","sourceRoot":"","sources":["in-memory-store.service.ts"],"names":[],"mappings":";AAAA;IAAA;IA+MA,CAAC;IA9MC;;;MAGE;IACF,uCAAQ,GAAR;QACE,IAAI,QAAQ,GAAG;YACb;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,WAAW;gBACnB,SAAS,EAAE,WAAW;aACvB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,KAAK;gBACb,SAAS,EAAE,MAAM;aAClB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,eAAe;gBACvB,SAAS,EAAE,OAAO;aACnB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,UAAU;gBAClB,SAAS,EAAE,SAAS;aACrB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,aAAa;gBACrB,SAAS,EAAE,SAAS;aACrB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,gBAAgB;gBACxB,SAAS,EAAE,iBAAiB;aAC7B;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,aAAa;gBACrB,SAAS,EAAE,WAAW;aACvB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,UAAU;gBAClB,SAAS,EAAE,eAAe;aAC3B;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,yBAAyB;gBACjC,SAAS,EAAE,SAAS;aACrB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,OAAO;gBACf,SAAS,EAAE,OAAO;aACnB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,KAAK;gBACb,SAAS,EAAE,WAAW;aACvB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,OAAO;gBACf,SAAS,EAAE,QAAQ;aACpB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,YAAY;gBACpB,SAAS,EAAE,SAAS;aACrB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,gBAAgB;gBACxB,SAAS,EAAE,QAAQ;aACpB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,aAAa;gBACrB,SAAS,EAAE,MAAM;aAClB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,eAAe;gBACvB,SAAS,EAAE,UAAU;aACtB;SACF,CAAC;QAEF,IAAI,QAAQ,GAAG;YACb;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,sBAAsB;gBAC9B,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,kBAAkB;gBAC1B,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,YAAY;gBACpB,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,UAAU;aACpB;YACD;gBACE,IAAI,EAAE,GAAG;gBACT,MAAM,EAAE,SAAS;gBACjB,OAAO,EAAE,UAAU;aACpB;SACF,CAAC;QAEF,IAAI,KAAK,GAAG;YACV;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,mBAAmB;gBAC3B,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,gBAAgB;gBACxB,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,yBAAyB;gBACjC,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,cAAc;gBACtB,MAAM,EAAE,MAAM;aACf;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,aAAa;gBACrB,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,gBAAgB;gBACxB,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,wBAAwB;gBAChC,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,iBAAiB;gBACzB,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,kBAAkB;gBAC1B,MAAM,EAAE,MAAM;aACf;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,cAAc;gBACtB,MAAM,EAAE,MAAM;aACf;YACD;gBACE,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,kBAAkB;gBAC1B,MAAM,EAAE,MAAM;aACf;SACF,CAAC;QAEF,MAAM,CAAC,EAAE,YAAK,EAAE,kBAAQ,EAAE,kBAAQ,EAAE,CAAC;IACvC,CAAC;IACH,2BAAC;AAAD,CAAC,AA/MD,IA+MC;AA/MY,4BAAoB,uBA+MhC,CAAA"} -------------------------------------------------------------------------------- /src/api/in-memory-store.service.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryDbService } from 'angular-in-memory-web-api'; 2 | 3 | export class InMemoryStoreService implements InMemoryDbService { 4 | /** 5 | * Creates fresh copy of data each time. 6 | * Safe for consuming service to morph arrays and objects. 7 | */ 8 | createDb() { 9 | const speakers = [ 10 | { 11 | id: 11, 12 | name: 'Chewbacca', 13 | twitter: '@im_chewy', 14 | nextId: 12, 15 | prevId: null 16 | }, 17 | { 18 | id: 12, 19 | name: 'Rey', 20 | twitter: '@rey', 21 | nextId: 13, 22 | prevId: 11 23 | }, 24 | { 25 | id: 13, 26 | name: 'Finn ', 27 | twitter: '@finn', 28 | nextId: 14, 29 | prevId: 12 30 | }, 31 | { 32 | id: 14, 33 | name: 'Han Solo', 34 | twitter: '@i_know', 35 | nextId: 15, 36 | prevId: 13 37 | }, 38 | { 39 | id: 15, 40 | name: 'Leia Organa', 41 | twitter: '@organa', 42 | nextId: 16, 43 | prevId: 14 44 | }, 45 | { 46 | id: 16, 47 | name: 'Luke Skywalker', 48 | twitter: '@chosen_one_son', 49 | nextId: 17, 50 | prevId: 15 51 | }, 52 | { 53 | id: 17, 54 | name: 'Poe Dameron', 55 | twitter: '@i_am_poe', 56 | nextId: 18, 57 | prevId: 16 58 | }, 59 | { 60 | id: 18, 61 | name: 'Kylo Ren', 62 | twitter: '@daddy_issues', 63 | nextId: 19, 64 | prevId: 17 65 | }, 66 | { 67 | id: 19, 68 | name: 'Supreme Leader Snoke', 69 | twitter: '@snoker', 70 | nextId: 20, 71 | prevId: 18 72 | }, 73 | { 74 | id: 20, 75 | name: 'R2-D2', 76 | twitter: '@r2d2', 77 | nextId: 21, 78 | prevId: 19 79 | }, 80 | { 81 | id: 21, 82 | name: 'BB8', 83 | twitter: '@bb_eight', 84 | nextId: 22, 85 | prevId: 20 86 | }, 87 | { 88 | id: 22, 89 | name: 'C-3PO', 90 | twitter: '@goldy', 91 | nextId: 23, 92 | prevId: 21 93 | }, 94 | { 95 | id: 23, 96 | name: 'Maz Kanata', 97 | twitter: '@mazzzy', 98 | nextId: 24, 99 | prevId: 22 100 | }, 101 | { 102 | id: 24, 103 | name: 'Captain Phasma', 104 | twitter: '@fazma', 105 | nextId: 25, 106 | prevId: 23 107 | }, 108 | { 109 | id: 25, 110 | name: 'General Hux', 111 | twitter: '@hux', 112 | nextId: 26, 113 | prevId: 24 114 | }, 115 | { 116 | id: 26, 117 | name: 'Lor San Tekka', 118 | twitter: '@lor_san', 119 | nextId: null, 120 | prevId: 25 121 | } 122 | ]; 123 | 124 | const sessions = [ 125 | { 126 | id: 130, 127 | name: 'Angular 2 First Look', 128 | level: 'beginner' 129 | }, 130 | { 131 | id: 132, 132 | name: 'RxJS', 133 | level: 'beginner' 134 | }, 135 | { 136 | id: 133, 137 | name: 'Angular Material', 138 | level: 'beginner' 139 | }, 140 | { 141 | id: 134, 142 | name: 'Redux', 143 | level: 'beginner' 144 | }, 145 | { 146 | id: 135, 147 | name: 'React', 148 | level: 'beginner' 149 | }, 150 | { 151 | id: 136, 152 | name: 'TypeScript', 153 | level: 'beginner' 154 | }, 155 | { 156 | id: 137, 157 | name: 'ES2015', 158 | level: 'beginner' 159 | }, 160 | { 161 | id: 138, 162 | name: 'Mongo', 163 | level: 'beginner' 164 | }, 165 | { 166 | id: 139, 167 | name: 'Redis', 168 | level: 'beginner' 169 | }, 170 | { 171 | id: 140, 172 | name: 'Node', 173 | level: 'beginner' 174 | }, 175 | { 176 | id: 141, 177 | name: 'Express', 178 | level: 'beginner' 179 | } 180 | ]; 181 | 182 | const rooms = [ 183 | { 184 | id: 30, 185 | name: 'Millennium Falcon', 186 | type: 'space' 187 | }, 188 | { 189 | id: 32, 190 | name: 'X-Wing Fighter', 191 | type: 'space' 192 | }, 193 | { 194 | id: 33, 195 | name: 'Imperial Star Destroyer', 196 | type: 'space' 197 | }, 198 | { 199 | id: 34, 200 | name: 'AT-AT Walker', 201 | type: 'land' 202 | }, 203 | { 204 | id: 35, 205 | name: 'TIE Fighter', 206 | type: 'space' 207 | }, 208 | { 209 | id: 36, 210 | name: 'B-Wing Fighter', 211 | type: 'space' 212 | }, 213 | { 214 | id: 37, 215 | name: 'ETA-2 Jedi Starfighter', 216 | type: 'space' 217 | }, 218 | { 219 | id: 38, 220 | name: 'TIE Interceptor', 221 | type: 'space' 222 | }, 223 | { 224 | id: 39, 225 | name: 'X-34 Landspeeder', 226 | type: 'land' 227 | }, 228 | { 229 | id: 40, 230 | name: 'Snow Speeder', 231 | type: 'land' 232 | }, 233 | { 234 | id: 41, 235 | name: 'X-34 Landspeeder', 236 | type: 'land' 237 | } 238 | ]; 239 | 240 | return { rooms, speakers, sessions }; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/api/sessions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 130, 4 | "name": "Angular2 First Look", 5 | "level": "beginner" 6 | }, 7 | { 8 | "id": 132, 9 | "name": "RxJS", 10 | "level": "beginner" 11 | }, 12 | { 13 | "id": 133, 14 | "name": "Angular Material", 15 | "level": "beginner" 16 | }, 17 | { 18 | "id": 134, 19 | "name": "Redux", 20 | "level": "beginner" 21 | }, 22 | { 23 | "id": 135, 24 | "name": "React", 25 | "level": "beginner" 26 | }, 27 | { 28 | "id": 136, 29 | "name": "TypeScript", 30 | "level": "beginner" 31 | }, 32 | { 33 | "id": 137, 34 | "name": "ES2015", 35 | "level": "beginner" 36 | }, 37 | { 38 | "id": 138, 39 | "name": "Mongo", 40 | "level": "beginner" 41 | }, 42 | { 43 | "id": 139, 44 | "name": "Redis", 45 | "level": "beginner" 46 | }, 47 | { 48 | "id": 140, 49 | "name": "Node", 50 | "level": "beginner" 51 | }, 52 | { 53 | "id": 141, 54 | "name": "Express", 55 | "level": "beginner" 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /src/api/speakers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 11, 4 | "name": "Chewbacca", 5 | "twitter": "@im_chewy" 6 | }, 7 | { 8 | "id": 12, 9 | "name": "Rey", 10 | "twitter": "@rey" 11 | }, 12 | { 13 | "id": 13, 14 | "name": "Finn", 15 | "twitter": "@finn" 16 | }, 17 | { 18 | "id": 14, 19 | "name": "Han Solo", 20 | "twitter": "@i_know" 21 | }, 22 | { 23 | "id": 15, 24 | "name": "Leia Organa", 25 | "twitter": "@organa" 26 | }, 27 | { 28 | "id": 16, 29 | "name": "Luke Skywalker", 30 | "twitter": "@chosen_one_son" 31 | }, 32 | { 33 | "id": 17, 34 | "name": "Poe Dameron", 35 | "twitter": "@i_am_poe" 36 | }, 37 | { 38 | "id": 18, 39 | "name": "Kylo Ren", 40 | "twitter": "@daddy_issues" 41 | }, 42 | { 43 | "id": 19, 44 | "name": "Supreme Leader Snoke", 45 | "twitter": "@snoker" 46 | }, 47 | { 48 | "id": 20, 49 | "name": "R2-D2", 50 | "twitter": "@r2d2" 51 | }, 52 | { 53 | "id": 21, 54 | "name": "BB8", 55 | "twitter": "@bb_eight" 56 | }, 57 | { 58 | "id": 22, 59 | "name": "C-3PO", 60 | "twitter": "@goldy" 61 | }, 62 | { 63 | "id": 23, 64 | "name": "Maz Kanata", 65 | "twitter": "@mazzzy" 66 | }, 67 | { 68 | "id": 24, 69 | "name": "Captain Phasma", 70 | "twitter": "@fazma" 71 | }, 72 | { 73 | "id": 25, 74 | "name": "General Hux", 75 | "twitter": "@hux" 76 | }, 77 | { 78 | "id": 26, 79 | "name": "Lor San Tekka", 80 | "twitter": "@lor_san" 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /src/app/admin/admin-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AdminComponent } from './admin.component'; 5 | 6 | const routes: Routes = [{ path: '', component: AdminComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule] 11 | }) 12 | export class AdminRoutingModule {} 13 | 14 | export const routedComponents = [AdminComponent]; 15 | -------------------------------------------------------------------------------- /src/app/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ev-admin', 5 | template: ` 6 |
7 |

Admin

8 | Welcome to the admin page! 9 |
10 | ` 11 | }) 12 | export class AdminComponent {} 13 | -------------------------------------------------------------------------------- /src/app/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AdminRoutingModule, routedComponents } from './admin-routing.module'; 4 | 5 | @NgModule({ 6 | imports: [AdminRoutingModule], 7 | declarations: [routedComponents] 8 | }) 9 | export class AdminModule {} 10 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .mdl-layout__content { 2 | margin-top: 68px; 3 | } 4 | 5 | .page-content { 6 | margin: 2em; 7 | } 8 | 9 | @media (max-width: 479px) { 10 | .page-content { 11 | margin-top: 4em; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture, async, fakeAsync, inject, tick } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { SpyNgModuleFactoryLoader } from '@angular/router/testing'; 5 | 6 | import { 7 | Component, 8 | DebugElement, 9 | NgModule, 10 | NgModuleFactoryLoader, 11 | NO_ERRORS_SCHEMA 12 | } from '@angular/core'; 13 | 14 | import { Location } from '@angular/common'; 15 | import { Router, RouterModule } from '@angular/router'; 16 | 17 | import { AppComponent } from './app.component'; 18 | import { routes } from './routes'; 19 | import { PageNotFoundComponent } from './core'; 20 | 21 | @Component({ 22 | template: '
lazy-loaded
' 23 | }) 24 | class LazyComponent {} 25 | 26 | @NgModule({ 27 | imports: [RouterModule.forChild([{ path: '', component: LazyComponent }])], 28 | declarations: [LazyComponent] 29 | }) 30 | class LazyModule {} 31 | 32 | describe('AppComponent', () => { 33 | beforeEach(() => { 34 | TestBed.configureTestingModule({ 35 | imports: [RouterTestingModule.withRoutes(routes)], 36 | declarations: [AppComponent, PageNotFoundComponent], 37 | schemas: [ 38 | /** 39 | * This tells the compiler to ignore any unknown elements 40 | * in the component template. This way we can only test 41 | * what we need to without bringing in all the dependencies. 42 | */ 43 | NO_ERRORS_SCHEMA 44 | ] 45 | }); 46 | TestBed.compileComponents(); 47 | }); 48 | 49 | it('true is true', () => expect(true).toBe(true)); 50 | 51 | it( 52 | 'should be defined', 53 | async(() => { 54 | const fixture = TestBed.createComponent(AppComponent); 55 | const app = fixture.debugElement.componentInstance; 56 | expect(app).toBeTruthy(); 57 | }) 58 | ); 59 | 60 | it('should contain a navigation component', () => { 61 | const fixture = TestBed.createComponent(AppComponent); 62 | const compiled = fixture.debugElement.nativeElement; 63 | expect(compiled.querySelectorAll('ev-nav').length).toBe(1); 64 | }); 65 | 66 | it( 67 | 'should render a 404 route', 68 | fakeAsync( 69 | inject([Router], (router: Router) => { 70 | const fixture = TestBed.createComponent(AppComponent); 71 | 72 | router.navigate(['/invalid']); 73 | 74 | tick(); 75 | fixture.detectChanges(); 76 | tick(); 77 | 78 | const compiled = fixture.debugElement.nativeElement; 79 | expect(compiled.querySelector('h4').textContent).toContain('Inconceivable!'); 80 | expect(compiled.querySelectorAll('ev-404').length).toBe(1); 81 | }) 82 | ) 83 | ); 84 | 85 | it( 86 | 'should lazy load a dashboard module', 87 | fakeAsync( 88 | inject( 89 | [Router, NgModuleFactoryLoader], 90 | (router: Router, loader: SpyNgModuleFactoryLoader) => { 91 | const fixture = TestBed.createComponent(AppComponent); 92 | 93 | loader.stubbedModules = { 94 | 'app/dashboard/dashboard.module#DashboardModule': LazyModule 95 | }; 96 | 97 | router.navigate(['/dashboard']); 98 | 99 | tick(); 100 | fixture.detectChanges(); 101 | tick(); 102 | 103 | const compiled = fixture.debugElement.nativeElement; 104 | expect(compiled.querySelector('div').textContent).toContain('lazy-loaded'); 105 | } 106 | ) 107 | ) 108 | ); 109 | 110 | it( 111 | 'should go to / on app creation', 112 | async( 113 | inject([Router, Location], (router: Router, location: Location) => { 114 | // fakeAsync(inject([Router, Location], (router: Router, location: Location) => { 115 | 116 | // const location = TestBed.get(Location); 117 | const fixture = TestBed.createComponent(AppComponent); 118 | const app = fixture.debugElement.componentInstance; 119 | expect(app).toBeTruthy(); 120 | expect(location.path()).toEqual(''); 121 | }) 122 | ) 123 | ); 124 | }); 125 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ev-app', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { RouterModule, NoPreloading } from '@angular/router'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { LoginModule } from './login/login.module'; 8 | import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; 9 | import { InMemoryStoreService } from '../api/in-memory-store.service'; 10 | import { QuicklinkStrategy, QuicklinkModule } from 'ngx-quicklink'; 11 | import { httpInterceptorProviders, declarations } from './core'; 12 | import { routes } from './routes'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | BrowserModule, 17 | HttpClientModule, 18 | LoginModule, 19 | QuicklinkModule, 20 | 21 | // Routes get loaded in order. 22 | // It is important that login comes before AppRoutingModule, 23 | // as AppRoutingModule defines the catch-all ** route 24 | RouterModule.forRoot( 25 | routes, 26 | /** 27 | * Preloading strategies: 28 | * - https://angular.io/guide/router#custom-preloading-strategy 29 | * 30 | * NoPreloading 31 | * - No bundles will preload 32 | * - built-in strategy 33 | * 34 | * PreloadAllModules 35 | * - All bundles will preload, automatically 36 | * - built-in strategy 37 | * - https://dev.to/angular/preload-all-angular-bundles-1b6l 38 | * 39 | * OptInPreloadStrategy 40 | * - set data.preload to true/false in the route configuration 41 | * - custom strategy 42 | * - https://dev.to/angular/you-pick-which-angular-bundles-to-preload-5l9 43 | * 44 | * NetworkAwarePreloadStrategy 45 | * - Customize which connections types to avoid 46 | * ['slow-2g', '2g', '3g', '4g' ] 47 | * - custom strategy 48 | * - https://dev.to/angular/preload-angular-bundles-when-good-network-connectivity-is-detected-j3a 49 | * 50 | * OnDemandPreloadStrategy 51 | * - Only preload when a specific event occurs. 52 | * - You control when it preloads and what preloads. 53 | * - Preload everything 54 | * this.preloadOnDemandService.startPreload('*'); 55 | * - Preload a specific bundle 56 | * this.preloadOnDemandService.startPreload(routePath); 57 | * - custom strategy 58 | * - https://dev.to/angular/predictive-preloading-strategy-for-your-angular-bundles-4bgl 59 | * 60 | * QuickLinkStrategy 61 | * - Looks for links on the viewable page. 62 | * - If they lead to a module, it preloads it (if not already loaded). 63 | * - npm i ngx-quicklink --save 64 | * - https://github.com/mgechev/ngx-quicklink 65 | */ 66 | { 67 | // enableTracing: true, 68 | preloadingStrategy: NoPreloading 69 | } 70 | ), 71 | InMemoryWebApiModule.forRoot(InMemoryStoreService, { delay: 10 }) 72 | ], 73 | declarations: [AppComponent, ...declarations], 74 | providers: [httpInterceptorProviders], 75 | bootstrap: [AppComponent] 76 | }) 77 | export class AppModule {} 78 | -------------------------------------------------------------------------------- /src/app/core/config.ts: -------------------------------------------------------------------------------- 1 | export const CONFIG = { 2 | baseUrls: { 3 | config: 'commands/config', 4 | resetDb: 'commands/resetDb', 5 | speakers: 'api/speakers', 6 | sessions: 'api/sessions' 7 | // speakers: 'api/speakers.json', 8 | // sessions: 'api/sessions.json' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/core/entity.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class EntityService { 5 | clone(source: T): T { 6 | return Object.assign({}, source); 7 | } 8 | 9 | merge = (target: any, ...sources: any[]) => Object.assign(target, ...sources); 10 | 11 | propertiesDiffer = (entityA: {}, entityB: {}) => 12 | Object.keys(entityA).find(key => entityA[key] !== entityB[key]); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/core/exception.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpErrorResponse } from '@angular/common/http'; 3 | import { Observable, of } from 'rxjs'; 4 | 5 | import { ToastService } from './toast.service'; 6 | 7 | @Injectable({providedIn: 'root'}) 8 | export class ExceptionService { 9 | constructor(private toastService: ToastService) {} 10 | 11 | catchBadResponse: (err: HttpErrorResponse | any) => Observable = ( 12 | err: any | HttpErrorResponse 13 | ) => { 14 | let emsg = ''; 15 | 16 | if (err.error instanceof Error) { 17 | // A client-side or network error occurred. Handle it accordingly. 18 | emsg = `An error occurred: ${err.error.message}`; 19 | } else { 20 | // The backend returned an unsuccessful response code. 21 | // The response body may contain clues as to what went wrong, 22 | emsg = `Backend returned code ${err.status}, body was: ${err.body.error}`; 23 | } 24 | 25 | // const emsg = err 26 | // ? err.error ? err.error : JSON.stringify(err) 27 | // : err.statusText || 'unknown error'; 28 | 29 | this.toastService.activate(`Error - Bad Response - ${emsg}`); 30 | // return Observable.throw(emsg); // TODO: We should NOT swallow error here. 31 | return of(false); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/core/guards/auth-load.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { CanMatchFn, Route, UrlSegment } from '@angular/router'; 3 | import { Router } from '@angular/router'; 4 | import { UserProfileService } from '../user-profile.service'; 5 | 6 | export const authLoadGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => { 7 | const deniedMessage = '💂‍♀️ [Guard] - Auth Guard - Unauthorized access denied'; 8 | 9 | const userProfileService = inject(UserProfileService); 10 | const router = inject(Router); 11 | if (userProfileService.isLoggedIn) { 12 | console.log(`💂‍♀️ [Guard] - Auth Load Guard - allowed`); 13 | return true; 14 | } 15 | 16 | const url = `/signin?redirectTo=/${route.path}`; 17 | const urlTree = router.parseUrl(url); 18 | router.navigateByUrl(urlTree); 19 | console.warn(deniedMessage); 20 | return userProfileService.isLoggedIn; 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/core/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivateFn, 5 | Router, 6 | RouterStateSnapshot, 7 | } from '@angular/router'; 8 | import { ToastService } from '../toast.service'; 9 | import { UserProfileService } from '../user-profile.service'; 10 | 11 | export const isAuthenticatedGuard: CanActivateFn = ( 12 | route: ActivatedRouteSnapshot, 13 | state: RouterStateSnapshot 14 | ) => { 15 | const deniedMessage = 'Unauthorized, access denied'; 16 | const userProfileService = inject(UserProfileService); 17 | const toastService = inject(ToastService); 18 | const router = inject(Router); 19 | 20 | if (userProfileService.isLoggedIn) { 21 | return true; 22 | } 23 | 24 | const url = `/login?redirectTo=${state.url}`; 25 | const urlTree = router.parseUrl(url); 26 | router.navigateByUrl(urlTree); 27 | 28 | toastService.activate(deniedMessage); 29 | return false; 30 | }; 31 | 32 | // old 33 | 34 | // import { Injectable } from '@angular/core'; 35 | // import { Route, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 36 | 37 | // import { ToastService } from '../toast.service'; 38 | // import { UserProfileService } from '../user-profile.service'; 39 | 40 | // @Injectable({ providedIn: 'root' }) 41 | // export class AuthGuard { 42 | // deniedMessage = 'Unauthorized access denied'; 43 | 44 | // constructor( 45 | // private userProfileService: UserProfileService, 46 | // private toastService: ToastService, 47 | // private router: Router 48 | // ) {} 49 | 50 | // canLoad(route: Route) { 51 | // if (this.userProfileService.isLoggedIn) { 52 | // return true; 53 | // } 54 | 55 | // const url = `/${route.path}`; 56 | // this.router.navigate(['/login'], { queryParams: { redirectTo: url } }); 57 | // this.toastService.activate(this.deniedMessage); 58 | // return this.userProfileService.isLoggedIn; 59 | // } 60 | 61 | // canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 62 | // if (this.userProfileService.isLoggedIn) { 63 | // return true; 64 | // } 65 | // this.router.navigate(['/login'], { 66 | // queryParams: { redirectTo: state.url }, 67 | // }); 68 | // this.toastService.activate(this.deniedMessage); 69 | // return false; 70 | // } 71 | 72 | // canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 73 | // return this.canActivate(route, state); 74 | // } 75 | // } 76 | -------------------------------------------------------------------------------- /src/app/core/guards/can-deactivate.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanDeactivateFn } from '@angular/router'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export interface CanComponentDeactivate { 5 | canDeactivate?: () => Observable | Promise | boolean; 6 | } 7 | 8 | export const canDeactivateGuard: CanDeactivateFn = ( 9 | component: CanComponentDeactivate 10 | ) => { 11 | if (component.canDeactivate) { 12 | let deactivate = component.canDeactivate(); 13 | return deactivate; 14 | } else { 15 | return true; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | import { ModalComponent } from './modal.component'; 2 | import { NavComponent } from './nav.component'; 3 | import { PageNotFoundComponent } from './page-not-found.component'; 4 | import { SpinnerComponent } from './spinner.component'; 5 | import { ToastComponent } from './toast.component'; 6 | 7 | export * from './guards/auth.guard'; 8 | export * from './guards/auth-load.guard'; 9 | export * from './guards/can-deactivate.guard'; 10 | export * from './config'; 11 | export * from './entity.service'; 12 | export * from './exception.service'; 13 | export * from './message.service'; 14 | export * from './modal.component'; 15 | export * from './modal.service'; 16 | export * from './models'; 17 | export * from './nav.component'; 18 | export * from './page-not-found.component'; 19 | export * from './spinner.component'; 20 | export * from './spinner.service'; 21 | export * from './interceptors'; 22 | export * from './toast.component'; 23 | export * from './toast.service'; 24 | export * from './strategies'; 25 | export * from './user-profile.service'; 26 | 27 | export const declarations = [ 28 | ModalComponent, 29 | NavComponent, 30 | PageNotFoundComponent, 31 | SpinnerComponent, 32 | ToastComponent, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/app/core/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AuthService } from './auth.service'; 6 | 7 | @Injectable() 8 | export class AuthInterceptor implements HttpInterceptor { 9 | constructor(private auth: AuthService) {} 10 | 11 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 12 | const authHeader = this.auth.getAuthorizationToken(); 13 | const authReq = req.clone({ 14 | setHeaders: { Authorization: authHeader, 'Content-Type': 'application/json' } 15 | }); 16 | 17 | console.log(`HTTP: Adding headers`); 18 | // Pass on the cloned request instead of the original request. 19 | return next.handle(authReq); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/core/interceptors/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AuthService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([AuthService], (service: AuthService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/core/interceptors/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class AuthService { 5 | getAuthorizationToken() { 6 | return [ 7 | 'Basic your-token-goes-here' 8 | // 'Authorization': 'Basic d2VudHdvcnRobWFuOkNoYW5nZV9tZQ==', 9 | // 'Accept': 'application/json;odata=verbose' 10 | ]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/core/interceptors/csrf.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class CSRFInterceptor implements HttpInterceptor { 7 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 8 | // let headers = req.headers; 9 | // // if (req.url.indexOf('http://your-url.azurewebsites.net') > -1) { 10 | // headers = req.headers.append('x-csrf-token', 'your-csrf-token-goes-here'); 11 | // // } 12 | // const clonedReq = req.clone({ headers }); 13 | const clonedReq = req.clone({ setHeaders: { 'x-csrf-token': 'your-csrf-token-goes-here' } }); 14 | console.log(`HTTP: Adding CSRF`); 15 | return next.handle(clonedReq); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/interceptors/ensure-ssl.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class EnsureSSLInterceptor implements HttpInterceptor { 7 | /** 8 | * Credit: https://angular.io/guide/http#http-interceptors 9 | */ 10 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 11 | // clone request and replace 'http://' with 'https://' at the same time 12 | const secureReq = req.clone({ 13 | url: req.url.replace('http://', 'https://') 14 | }); 15 | // send the cloned, "secure" request to the next handler. 16 | console.log(`HTTP: Rerouting all traffic to SSL`); 17 | return next.handle(secureReq); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/core/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { AuthInterceptor } from './auth.interceptor'; 3 | import { CSRFInterceptor } from './csrf.interceptor'; 4 | import { TransformResponseInterceptor } from './transform-response.interceptor'; 5 | import { LogResponseTimeInterceptor } from './log-response.interceptor'; 6 | import { LogHeadersInterceptor } from './log-headers.interceptor'; 7 | import { EnsureSSLInterceptor } from './ensure-ssl.interceptor'; 8 | 9 | export * from './auth.interceptor'; 10 | export * from './csrf.interceptor'; 11 | export * from './ensure-ssl.interceptor'; 12 | export * from './log-headers.interceptor'; 13 | export * from './log-response.interceptor'; 14 | export * from './transform-response.interceptor'; 15 | 16 | export const httpInterceptorProviders = [ 17 | { provide: HTTP_INTERCEPTORS, useClass: EnsureSSLInterceptor, multi: true }, 18 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 19 | { provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true }, 20 | { provide: HTTP_INTERCEPTORS, useClass: TransformResponseInterceptor, multi: true }, 21 | { provide: HTTP_INTERCEPTORS, useClass: LogResponseTimeInterceptor, multi: true }, 22 | { provide: HTTP_INTERCEPTORS, useClass: LogHeadersInterceptor, multi: true } 23 | ]; 24 | 25 | /** 26 | * https://angular.io/guide/http#http-interceptors 27 | * 28 | * Why you care? 29 | * Have you ever needed to add headers to all or a subset of http requests? Transform the response? Log specific requests? 30 | * Without interception, developers would have to implement these tasks explicitly for each HttpClient method call. 31 | * 32 | * What is an interceptor? 33 | * HTTP Interception is a major feature of @angular/common/http. With interception, you declare interceptors that inspect and transform HTTP requests from your application to the server. The same interceptors may also inspect and transform the server's responses on their way back to the application. Multiple interceptors form a forward-and-backward chain of request/response handlers. 34 | * Interceptors can perform a variety of implicit tasks, from authentication to logging, in a routine, standard way, for every HTTP request/response. 35 | * 36 | * Providing Interceptors 37 | * Because interceptors are (optional) dependencies of the HttpClient service, you must provide them in the same injector (or a parent of the injector) that provides HttpClient. Interceptors provided after DI creates the HttpClient are ignored. 38 | * Use a barrel and export an array of the interceptors. 39 | * 40 | * Modifying Requests 41 | * Although interceptors are capable of mutating requests and responses, the HttpRequest and HttpResponse instance properties are readonly, rendering them largely immutable. 42 | * The request is immutable ... you must clone it. 43 | * Most interceptors transform the outgoing request before passing it to the next interceptor in the chain, by calling next.handle(transformedReq). An interceptor may transform the response event stream as well, by applying additional RxJS operators on the stream returned by next.handle(). 44 | * 45 | * Modifing Responses 46 | * After you return stream via next.handle(), you can pipe the stream's response. Use RxJS operators to transform or do what you will to it. 47 | * The response is immutable ... you must clone it. 48 | * 49 | * What is multi true? 50 | * The multi: true option is a required setting that tells Angular that HTTP_INTERCEPTORS is a token for a multiprovider that injects an array of values, rather than a single value. 51 | * 52 | * Order is important 53 | * Angular applies interceptors in the order that you provide them. If you provide interceptors A, then B, then C, requests will flow in A->B->C and responses will flow out C->B->A. 54 | 55 | 56 | 57 | */ 58 | -------------------------------------------------------------------------------- /src/app/core/interceptors/log-headers.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent } from '@angular/common/http'; 2 | import { Observable } from 'rxjs'; 3 | import { Injectable } from '@angular/core'; 4 | 5 | @Injectable() 6 | export class LogHeadersInterceptor implements HttpInterceptor { 7 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 8 | console.log(`HTTP: Log headers:`); 9 | let headerList: { key: string; values: string }[] = []; 10 | req.headers.keys().map((key) => { 11 | headerList.push({ key, values: (req.headers.getAll(key) || '').toString() }); 12 | }); 13 | console.table(headerList); 14 | return next.handle(req); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/core/interceptors/log-response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpInterceptor, 3 | HttpHandler, 4 | HttpRequest, 5 | HttpEvent, 6 | HttpResponse 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { tap, finalize } from 'rxjs/operators'; 10 | import { Injectable } from "@angular/core"; 11 | 12 | @Injectable() 13 | export class LogResponseTimeInterceptor implements HttpInterceptor { 14 | /** 15 | * Credit: https://angular.io/guide/http#http-interceptors 16 | */ 17 | // intercept_alternative(req: HttpRequest, next: HttpHandler): Observable> { 18 | // const started = Date.now(); 19 | // return next.handle(req).pipe( 20 | // tap(event => { 21 | // if (event instanceof HttpResponse) { 22 | // const elapsed = Date.now() - started; 23 | // console.log(`HTTP: Request for ${req.urlWithParams} took ${elapsed} ms.`); 24 | // } 25 | // }) 26 | // ); 27 | // } 28 | 29 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 30 | const started = Date.now(); 31 | let ok: string; 32 | 33 | return next.handle(req).pipe( 34 | tap( 35 | // Succeeds when there is a response; ignore other events 36 | event => (ok = event instanceof HttpResponse ? 'succeeded' : ''), 37 | // Operation failed; error is an HttpErrorResponse 38 | error => (ok = 'failed') 39 | ), 40 | // Log when response observable either completes or errors 41 | finalize(() => { 42 | const elapsed = Date.now() - started; 43 | console.log(`${req.method} "${req.urlWithParams}" \n\t ${ok} in ${elapsed} ms.`); 44 | }) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/core/interceptors/transform-response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpInterceptor, 3 | HttpHandler, 4 | HttpRequest, 5 | HttpEvent, 6 | HttpResponse, 7 | } from '@angular/common/http'; 8 | import { Observable } from 'rxjs'; 9 | import { map } from 'rxjs/operators'; 10 | import { Injectable } from '@angular/core'; 11 | 12 | @Injectable() 13 | export class TransformResponseInterceptor implements HttpInterceptor { 14 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 15 | return next.handle(req).pipe( 16 | map((event) => { 17 | 18 | if (event instanceof HttpResponse) { 19 | if (event.url && event.url.indexOf('speakers') >= 0 && Array.isArray(event.body)) { 20 | let body = event.body.map((speaker) => { 21 | if (speaker.name.match(/rey/i)) { 22 | speaker.name = 'Rey Skywalker'; 23 | } 24 | return speaker; 25 | }); 26 | console.log(`HTTP: Request transformed`); 27 | return event.clone({ body }); 28 | } 29 | return event.clone(); // undefined means dont change it 30 | // return event.clone(undefined); // undefined means dont change it 31 | } 32 | return event; 33 | }) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/core/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Subject } from 'rxjs'; 4 | 5 | import { CONFIG } from './config'; 6 | import { ToastService } from './toast.service'; 7 | 8 | export interface ResetMessage { 9 | message: string; 10 | } 11 | 12 | @Injectable({ providedIn: 'root' }) 13 | export class MessageService { 14 | private subject = new Subject(); 15 | 16 | state = this.subject.asObservable(); 17 | 18 | constructor(private http: HttpClient, private toastService: ToastService) {} 19 | 20 | resetDb() { 21 | const msg = 'Reset the Data Successfully'; 22 | this.http.post(CONFIG.baseUrls.resetDb, null).subscribe(() => { 23 | this.subject.next({ message: msg }); 24 | this.toastService.activate(msg); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/core/modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ModalService } from './modal.service'; 4 | 5 | const KEY_ESC = 27; 6 | 7 | @Component({ 8 | selector: 'ev-modal', 9 | template: ` 10 |
11 |
12 |
{{ title }}
13 |

{{ message }}

14 |
15 | 23 | 31 |
32 |
33 |
34 | `, 35 | styles: [ 36 | ` 37 | .dialog-container, 38 | .loading-container { 39 | position: absolute; 40 | top: 0; 41 | right: 0; 42 | bottom: 0; 43 | left: 0; 44 | overflow: scroll; 45 | background: rgba(0, 0, 0, 0.4); 46 | z-index: 0; 47 | opacity: 0; 48 | -webkit-transition: opacity 400ms ease-in; 49 | -moz-transition: opacity 400ms ease-in; 50 | transition: opacity 400ms ease-in; 51 | } 52 | 53 | .dialog-container > div { 54 | position: relative; 55 | width: 90%; 56 | max-width: 500px; 57 | min-height: 25px; 58 | margin: 10% auto; 59 | z-index: 99999; 60 | padding: 16px 16px 0; 61 | } 62 | 63 | .dialog-button-bar { 64 | text-align: right; 65 | margin-top: 8px; 66 | } 67 | 68 | .loading-container > div { 69 | position: relative; 70 | width: 50px; 71 | height: 50px; 72 | margin: 10% auto; 73 | z-index: 99999; 74 | } 75 | 76 | .loading-container > div > div { 77 | width: 100%; 78 | height: 100%; 79 | } 80 | 81 | .dialog-container .dialog-button-bar button { 82 | margin: 0 0 0 1em; 83 | } 84 | `, 85 | ], 86 | }) 87 | export class ModalComponent implements OnInit { 88 | title: string; 89 | message: string; 90 | okText: string; 91 | cancelText: string; 92 | negativeOnClick: (e: any) => void; 93 | positiveOnClick: (e: any) => void; 94 | 95 | private defaults = { 96 | title: 'Confirmation', 97 | message: 'Do you want to cancel your changes?', 98 | cancelText: 'Cancel', 99 | okText: 'OK', 100 | }; 101 | private modalElement: any; 102 | private cancelButton: any; 103 | private okButton: any; 104 | 105 | constructor(modalService: ModalService) { 106 | modalService.activate = this.activate.bind(this); 107 | } 108 | 109 | activate( 110 | // title = this.defaults.title, 111 | message = this.defaults.message 112 | // cancelText = this.defaults.cancelText, 113 | // okText = this.defaults.okText 114 | ) { 115 | this.title = this.defaults.title; 116 | this.message = message; 117 | this.cancelText = this.defaults.cancelText; 118 | this.okText = this.defaults.okText; 119 | 120 | const promise = new Promise((resolve, reject) => { 121 | this.negativeOnClick = (e: any) => resolve(false); 122 | this.positiveOnClick = (e: any) => resolve(true); 123 | this.show(); 124 | }); 125 | 126 | return promise; 127 | } 128 | 129 | ngOnInit() { 130 | this.modalElement = document.getElementById('confirmationModal'); 131 | this.cancelButton = document.getElementById('cancelButton'); 132 | this.okButton = document.getElementById('okButton'); 133 | } 134 | 135 | private show() { 136 | document.onkeyup = null; 137 | 138 | if (!this.modalElement || !this.cancelButton || !this.okButton) { 139 | return; 140 | } 141 | 142 | this.modalElement.style.opacity = 0; 143 | this.modalElement.style.zIndex = 9999; 144 | 145 | this.cancelButton.onclick = (e: any) => { 146 | e.preventDefault(); 147 | this.negativeOnClick(e); 148 | // if (!this.negativeOnClick(e)) { 149 | this.hideDialog(); 150 | // } 151 | }; 152 | 153 | this.okButton.onclick = (e: any) => { 154 | e.preventDefault(); 155 | this.positiveOnClick(e); 156 | // if (!this.positiveOnClick(e)) { 157 | // this.hideDialog(); 158 | // } 159 | }; 160 | 161 | this.modalElement.onclick = () => { 162 | this.hideDialog(); 163 | return this.negativeOnClick(null); 164 | }; 165 | 166 | document.onkeyup = (e: any) => { 167 | if (e.which === KEY_ESC) { 168 | this.hideDialog(); 169 | return this.negativeOnClick(null); 170 | } 171 | }; 172 | 173 | this.modalElement.style.opacity = 1; 174 | } 175 | 176 | private hideDialog() { 177 | document.onkeyup = null; 178 | this.modalElement.style.opacity = 0; 179 | window.setTimeout(() => (this.modalElement.style.zIndex = 0), 400); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/app/core/modal.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class ModalService { 5 | activate: (message?: string) => Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/core/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './speaker.model'; 2 | export * from './speaker.service'; 3 | -------------------------------------------------------------------------------- /src/app/core/models/speaker.model.ts: -------------------------------------------------------------------------------- 1 | export class Speaker { 2 | prevId: number; 3 | nextId: number; 4 | constructor(public id: number, public name: string, public twitter: string) {} 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/models/speaker.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { catchError, finalize, map } from 'rxjs/operators'; 5 | 6 | import { Speaker } from './speaker.model'; 7 | import { CONFIG } from '../config'; 8 | import { ExceptionService } from '../exception.service'; 9 | import { MessageService } from '../message.service'; 10 | import { SpinnerService } from '../spinner.service'; 11 | 12 | const speakersUrl = CONFIG.baseUrls.speakers; 13 | 14 | @Injectable({ providedIn: 'root' }) 15 | export class SpeakerService { 16 | onDbReset = this.messageService.state; 17 | 18 | // const catchHttpErrors = () => (source$: Observable) => 19 | private catchHttpErrors = () => (source$: Observable) => 20 | source$.pipe( 21 | catchError(this.exceptionService.catchBadResponse), 22 | finalize(() => this.spinnerService.hide()) 23 | ); 24 | 25 | constructor( 26 | private http: HttpClient, 27 | private exceptionService: ExceptionService, 28 | private messageService: MessageService, 29 | private spinnerService: SpinnerService 30 | ) { 31 | this.messageService.state.subscribe(state => this.getSpeakers()); 32 | } 33 | 34 | addSpeaker(speaker: Speaker): Observable { 35 | this.spinnerService.show(); 36 | return this.http.post(`${speakersUrl}`, speaker).pipe(this.catchHttpErrors()); 37 | } 38 | 39 | deleteSpeaker(speaker: Speaker): Observable { 40 | this.spinnerService.show(); 41 | return this.http.delete(`${speakersUrl}/${speaker.id}`).pipe(this.catchHttpErrors()); 42 | } 43 | 44 | getSpeakers(): Observable { 45 | this.spinnerService.show(); 46 | return this.http.get(speakersUrl).pipe( 47 | map(speakers => this.sortSpeakers(speakers)), 48 | this.catchHttpErrors() 49 | ); 50 | } 51 | 52 | sortSpeakers(speakers: Speaker[]) { 53 | return speakers.sort((a: Speaker, b: Speaker) => { 54 | if (a.name < b.name) { 55 | return -1; 56 | } 57 | if (a.name > b.name) { 58 | return 1; 59 | } 60 | return 0; 61 | }); 62 | } 63 | 64 | getSpeaker(id: number) { 65 | this.spinnerService.show(); 66 | return this.http.get(speakersUrl).pipe( 67 | map(speakers => speakers.find(speaker => speaker.id === id)), 68 | this.catchHttpErrors() 69 | ); 70 | /** 71 | * TODO: 72 | * When using JSON, we need the map above. 73 | * When using a DB, we use http, as shown below 74 | */ 75 | // return this.http.get(`${speakersUrl}/${id}`).pipe(this.catchHttpErrors()); 76 | } 77 | 78 | updateSpeaker(speaker: Speaker): Observable { 79 | this.spinnerService.show(); 80 | 81 | return this.http 82 | .put(`${speakersUrl}/${speaker.id}`, speaker) 83 | .pipe(this.catchHttpErrors()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/core/module-import-check.ts: -------------------------------------------------------------------------------- 1 | export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) { 2 | if (parentModule) { 3 | const msg = `${moduleName} has already been loaded. Import Core modules in the AppModule only.`; 4 | throw new Error(msg); 5 | } 6 | } 7 | 8 | // @NgModule({ 9 | // export class CoreModule { 10 | // constructor( 11 | // @Optional() 12 | // @SkipSelf() 13 | // parentModule: CoreModule 14 | // ) { 15 | // throwIfAlreadyLoaded(parentModule, 'CoreModule'); 16 | // } 17 | // } 18 | -------------------------------------------------------------------------------- /src/app/core/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { ModalService } from './modal.service'; 4 | import { MessageService } from './message.service'; 5 | import { OnDemandPreloadService } from './strategies'; 6 | 7 | class MenuItem { 8 | constructor(public caption: string, public path: string, public link: string) {} 9 | } 10 | 11 | @Component({ 12 | selector: 'ev-nav', 13 | template: ` 14 |
15 |
16 | 17 |

Event View

18 | 32 |
33 | 45 |
46 | `, 47 | styles: [ 48 | ` 49 | .mdl-layout__header { 50 | display: flex; 51 | position: fixed; 52 | background-color: #222; 53 | } 54 | 55 | .nav-link { 56 | padding: 0 1em; 57 | width: 100px; 58 | color: rgba(255, 255, 255, 0.6); 59 | text-align: center; 60 | text-decoration: none; 61 | } 62 | 63 | .nav-link.router-link-active { 64 | color: rgba(255, 255, 255, 1); 65 | } 66 | 67 | .nav-link.router-link-active::after { 68 | height: 3px; 69 | width: 100%; 70 | display: block; 71 | content: ' '; 72 | bottom: 0; 73 | left: 0; 74 | position: inherit; 75 | background: rgb(83, 109, 254); 76 | } 77 | 78 | .md-title-icon > i { 79 | background-image: url('/assets/ng.png'); 80 | background-repeat: no-repeat; 81 | background-position: center center; 82 | padding: 1em 2em; 83 | } 84 | 85 | .mdl-layout__header-row { 86 | height: 56px; 87 | padding: 0 16px 0 72px; 88 | padding-left: 8px; 89 | background-color: #673ab7; 90 | background: #0033ff; 91 | background-color: #222; 92 | } 93 | 94 | .nav-buttons-right { 95 | position: fixed; 96 | right: 2em; 97 | top: 1em; 98 | } 99 | .nav-buttons-right > button { 100 | margin-right: 8px; 101 | } 102 | 103 | @media (max-width: 480px) { 104 | #nav-buttons-right { 105 | display: none; 106 | } 107 | } 108 | 109 | @media (max-width: 320px) { 110 | a.nav-link { 111 | font-size: 12px; 112 | } 113 | } 114 | `, 115 | ], 116 | }) 117 | export class NavComponent implements OnInit { 118 | menuItems: MenuItem[]; 119 | 120 | ngOnInit() { 121 | this.menuItems = [ 122 | { caption: 'Dashboard', path: 'dashboard', link: '/dashboard' }, 123 | { caption: 'Speakers', path: 'speakers', link: '/speakers' }, 124 | { caption: 'Sessions', path: 'sessions', link: '/sessions' }, 125 | { caption: 'Admin', path: 'admin', link: '/admin' }, 126 | { caption: 'Login', path: 'login', link: '/login' }, 127 | ]; 128 | } 129 | 130 | constructor( 131 | private messageService: MessageService, 132 | private modalService: ModalService, 133 | private preloadOnDemandService: OnDemandPreloadService 134 | ) {} 135 | 136 | preloadAll() { 137 | this.preloadOnDemandService.startPreload('*'); 138 | } 139 | 140 | preloadBundle(routePath) { 141 | this.preloadOnDemandService.startPreload(routePath); 142 | } 143 | 144 | resetDb() { 145 | // console.log('*** The "Reset DB" is disabled until in memory API is re-enabled'); 146 | const msg = 'Are you sure you want to reset the database?'; 147 | this.modalService.activate(msg).then((responseOK) => { 148 | if (responseOK) { 149 | this.messageService.resetDb(); 150 | } 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/app/core/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ev-404', 5 | template: ` 6 |
7 |

Inconceivable!

8 |
I do not think this page is where you think it is.
9 |
10 | ` 11 | }) 12 | export class PageNotFoundComponent {} 13 | -------------------------------------------------------------------------------- /src/app/core/spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | 4 | import { SpinnerState, SpinnerService } from './spinner.service'; 5 | 6 | @Component({ 7 | selector: 'ev-spinner', 8 | template: ` 9 |
13 | `, 14 | styles: [ 15 | ` 16 | .spinner { 17 | position: absolute; 18 | left: 46%; 19 | top: 12%; 20 | } 21 | ` 22 | ] 23 | }) 24 | export class SpinnerComponent implements OnDestroy, OnInit { 25 | visible = false; 26 | private subs = new Subscription(); 27 | 28 | constructor(private spinnerService: SpinnerService) {} 29 | 30 | ngOnInit() { 31 | componentHandler.upgradeDom(); 32 | this.subs.add( 33 | this.spinnerService.spinnerState.subscribe( 34 | (state: SpinnerState) => (this.visible = state.show) 35 | ) 36 | ); 37 | } 38 | 39 | ngOnDestroy() { 40 | this.subs.unsubscribe(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/core/spinner.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Optional, SkipSelf } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | export interface SpinnerState { 5 | show: boolean; 6 | } 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class SpinnerService { 10 | private spinnerSubject = new Subject(); 11 | 12 | spinnerState = this.spinnerSubject.asObservable(); 13 | 14 | constructor( 15 | @Optional() 16 | @SkipSelf() 17 | prior: SpinnerService 18 | ) { 19 | if (prior) { 20 | return prior; 21 | } 22 | console.log('created spinner service'); 23 | } 24 | 25 | show() { 26 | this.spinnerSubject.next({ show: true }); 27 | } 28 | 29 | hide() { 30 | this.spinnerSubject.next({ show: false }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/core/strategies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './network-aware-preload-strategy'; 2 | export * from './on-demand-preload-strategy'; 3 | export * from './on-demand-preload.service'; 4 | export * from './opt-in-preload-strategy'; 5 | -------------------------------------------------------------------------------- /src/app/core/strategies/network-aware-preload-strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PreloadingStrategy, Route } from '@angular/router'; 3 | import { Observable, EMPTY } from 'rxjs'; 4 | 5 | // avoid typing issues for now 6 | export declare var navigator; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class NetworkAwarePreloadStrategy implements PreloadingStrategy { 10 | preload(route: Route, load: () => Observable): Observable { 11 | return this.hasGoodConnection() ? load() : EMPTY; 12 | } 13 | 14 | hasGoodConnection(): boolean { 15 | const conn = navigator.connection; 16 | if (conn) { 17 | if (conn.saveData) { 18 | return false; // save data mode is enabled, so dont preload 19 | } 20 | const avoidTheseConnections = ['slow-2g', '2g' /* , '3g', '4g' */]; 21 | const effectiveType = conn.effectiveType || ''; 22 | console.log(effectiveType); 23 | if (avoidTheseConnections.includes(effectiveType)) { 24 | return false; 25 | } 26 | } 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/core/strategies/on-demand-preload-strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PreloadingStrategy, Route } from '@angular/router'; 3 | import { Observable, EMPTY } from 'rxjs'; 4 | import { mergeMap } from 'rxjs/operators'; 5 | import { OnDemandPreloadService, OnDemandPreloadOptions } from './on-demand-preload.service'; 6 | 7 | @Injectable({ providedIn: 'root', deps: [OnDemandPreloadService] }) 8 | export class OnDemandPreloadStrategy implements PreloadingStrategy { 9 | private preloadOnDemand$: Observable; 10 | 11 | constructor(private preloadOnDemandService: OnDemandPreloadService) { 12 | this.preloadOnDemand$ = this.preloadOnDemandService.state; 13 | } 14 | 15 | preload(route: Route, load: () => Observable): Observable { 16 | return this.preloadOnDemand$.pipe( 17 | /** 18 | * Using mergeMap because order is not important, 19 | * and we do not want to cancel previous one. 20 | * switchMap could cancel previous call. 21 | * concatMap would make the multiple calls wait for each other. 22 | */ 23 | mergeMap(preloadOptions => { 24 | const shouldPreload = this.preloadCheck(route, preloadOptions); 25 | console.log(`${shouldPreload ? '' : 'Not '}Preloading ${route.path}`); 26 | return shouldPreload ? load() : EMPTY; 27 | }) 28 | ); 29 | } 30 | 31 | private preloadCheck(route: Route, preloadOptions: OnDemandPreloadOptions) { 32 | return ( 33 | route.data && 34 | route.data['preload'] && 35 | [route.path, '*'].includes(preloadOptions.routePath) && 36 | preloadOptions.preload 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/core/strategies/on-demand-preload.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | export class OnDemandPreloadOptions { 5 | constructor(public routePath: string, public preload = true) {} 6 | } 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class OnDemandPreloadService { 10 | private subject = new Subject(); 11 | state = this.subject.asObservable(); 12 | 13 | startPreload(routePath: string) { 14 | const message = new OnDemandPreloadOptions(routePath, true); 15 | this.subject.next(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/strategies/opt-in-preload-strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PreloadingStrategy, Route } from '@angular/router'; 3 | import { Observable, EMPTY } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class OptInPreloadStrategy implements PreloadingStrategy { 7 | preload(route: Route, load: () => Observable): Observable { 8 | return route.data && route.data['preload'] ? load() : EMPTY; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/core/toast.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ToastService } from './toast.service'; 3 | 4 | import { Subscription } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'ev-toast', 8 | template: ` 9 |
10 |
11 |
{{ title }}
12 |

{{ message }}

13 |
14 |
15 | `, 16 | styles: [ 17 | ` 18 | .toast-container { 19 | position: absolute; 20 | right: 0; 21 | bottom: 0; 22 | left: 0; 23 | overflow: scroll; 24 | background: rgba(0, 0, 0, 0.4); 25 | z-index: 9999; 26 | opacity: 0; 27 | 28 | -webkit-transition: opacity 400ms ease-in; 29 | -moz-transition: opacity 400ms ease-in; 30 | transition: opacity 400ms ease-in; 31 | } 32 | 33 | .toast-container > * { 34 | text-align: center; 35 | } 36 | 37 | .toast-card { 38 | width: 100%; 39 | z-index: 1; 40 | padding: 2px; 41 | position: relative; 42 | background-color: rgb(255, 64, 129); 43 | background-color: #f06292; 44 | background-color: rgb(103, 58, 183); 45 | background-color: rgb(83, 109, 254); 46 | text-align: center; 47 | color: white; 48 | } 49 | 50 | .toast-card .toast-message { 51 | margin: 0em 2em 1em 2em; 52 | } 53 | 54 | .toast-card .toast-title { 55 | text-transform: uppercase; 56 | margin: 16px; 57 | font-size: 18px; 58 | } 59 | ` 60 | ] 61 | }) 62 | export class ToastComponent implements OnDestroy, OnInit { 63 | private subs = new Subscription(); 64 | private defaults = { 65 | title: '', 66 | message: 'May the Force be with You' 67 | }; 68 | private toastElement: any; 69 | 70 | title: string; 71 | message: string; 72 | 73 | constructor(private toastService: ToastService) { 74 | this.subs.add( 75 | this.toastService.toastState.subscribe(toastMessage => { 76 | console.log(`activiting toast: ${toastMessage.message}`); 77 | this.activate(toastMessage.message); 78 | }) 79 | ); 80 | } 81 | 82 | activate(message = this.defaults.message, title = this.defaults.title) { 83 | this.title = title; 84 | this.message = message; 85 | this.show(); 86 | } 87 | 88 | ngOnInit() { 89 | this.toastElement = document.getElementById('toast'); 90 | } 91 | 92 | ngOnDestroy() { 93 | this.subs.unsubscribe(); 94 | } 95 | 96 | private show() { 97 | console.log(this.message); 98 | this.toastElement.style.opacity = 1; 99 | this.toastElement.style.zIndex = 9999; 100 | 101 | window.setTimeout(() => this.hide(), 2500); 102 | } 103 | 104 | private hide() { 105 | this.toastElement.style.opacity = 0; 106 | window.setTimeout(() => (this.toastElement.style.zIndex = 0), 400); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/core/toast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Optional, SkipSelf } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | export interface ToastMessage { 5 | message: string; 6 | } 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class ToastService { 10 | private toastSubject = new Subject(); 11 | 12 | toastState = this.toastSubject.asObservable(); 13 | 14 | constructor( 15 | @Optional() 16 | @SkipSelf() 17 | prior: ToastService 18 | ) { 19 | if (prior) { 20 | console.log('toast service already exists'); 21 | return prior; 22 | } else { 23 | console.log('created toast service'); 24 | } 25 | } 26 | 27 | activate(message?: string) { 28 | this.toastSubject.next({ message: message }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/core/user-profile.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class UserProfileService { 5 | isLoggedIn = false; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { DashboardComponent } from './dashboard.component'; 5 | 6 | const routes: Routes = [ 7 | { path: '', component: DashboardComponent, data: { title: 'Top Speakers' } } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class DashboardRoutingModule {} 15 | 16 | export const routedComponents = [DashboardComponent]; 17 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.css: -------------------------------------------------------------------------------- 1 | .mdl-cell--col-3 { 2 | width: 25%; 3 | } 4 | 5 | @media (max-width: 839px) and (min-width: 480px) { 6 | .mdl-cell.mdl-cell--3-col { 7 | width: 25%; 8 | } 9 | } 10 | 11 | @media (max-width: 479px) and (min-width: 320px) { 12 | .mdl-cell.mdl-cell--3-col { 13 | width: 10em; 14 | margin: 0.5em auto; 15 | } 16 | } 17 | 18 | @media (max-width: 319px) { 19 | .mdl-cell.mdl-cell--3-col { 20 | width: 10em; 21 | margin: 0.5em auto; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ title }}

3 |
4 |
5 |
10 | 11 |
12 |
13 | 14 | Loading the Dashboard ... 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Observable, of, Subject, Subscription } from 'rxjs'; 4 | import { catchError, tap } from 'rxjs/operators'; 5 | 6 | import { Speaker, SpeakerService, ToastService } from '../core'; 7 | 8 | @Component({ 9 | selector: 'ev-dashboard', 10 | templateUrl: './dashboard.component.html', 11 | styleUrls: ['./dashboard.component.css'] 12 | }) 13 | export class DashboardComponent implements OnDestroy, OnInit { 14 | /** 15 | * Here we are using an Observable<> so we can use the async pipe in the 16 | * template. Whether you use the async pipe or not, be consistent. 17 | */ 18 | private subs = new Subscription(); 19 | speakers$: Observable; 20 | title: string; 21 | 22 | constructor( 23 | private route: ActivatedRoute, 24 | private speakerService: SpeakerService, 25 | private router: Router, 26 | private toastService: ToastService 27 | ) {} 28 | 29 | getSpeakers() { 30 | this.speakers$ = this.speakerService.getSpeakers().pipe( 31 | tap(() => this.toastService.activate('Got speakers for the dashboard')), 32 | catchError(e => { 33 | this.toastService.activate(`${e}`); 34 | return of([]); 35 | }) 36 | ); 37 | } 38 | 39 | gotoDetail(speaker: Speaker) { 40 | const link = ['/speakers', speaker.id]; 41 | this.router.navigate(link); 42 | } 43 | 44 | ngOnDestroy() { 45 | this.subs.unsubscribe(); 46 | } 47 | 48 | ngOnInit() { 49 | this.route.data.subscribe((data: { title: string }) => { 50 | this.title = data.title; 51 | }); 52 | this.getSpeakers(); 53 | this.subs.add(this.speakerService.onDbReset.subscribe(() => this.getSpeakers())); 54 | } 55 | 56 | trackBySpeakers(index: number, speaker: Speaker) { 57 | return speaker.id; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { DashboardButtonComponent } from './shared/dashboard-button/dashboard-button.component'; 4 | import { DashboardRoutingModule, routedComponents } from './dashboard-routing.module'; 5 | import { SharedModule } from '../shared/shared.module'; 6 | 7 | @NgModule({ 8 | imports: [DashboardRoutingModule, SharedModule], 9 | declarations: [DashboardButtonComponent, routedComponents] 10 | }) 11 | export class DashboardModule {} 12 | -------------------------------------------------------------------------------- /src/app/dashboard/shared/dashboard-button/dashboard-button.component.css: -------------------------------------------------------------------------------- 1 | button { 2 | width: 200px; 3 | height: 70px; 4 | /*border-bottom: rgb(83,109,254) 4px solid;*/ 5 | /*color: #111;*/ 6 | background-color: rgb(51, 66, 150); 7 | color: #eee; 8 | } 9 | 10 | .mdl-button:hover { 11 | /*background-color: rgba(158,158,158,.2);*/ 12 | background-color: #448aff; 13 | } 14 | 15 | @media (max-width: 1024px) { 16 | button { 17 | width: 180px; 18 | line-height: 18px; 19 | font-size: 12px; 20 | } 21 | } 22 | 23 | @media (max-width: 839px) { 24 | button { 25 | width: 120px; 26 | line-height: 18px; 27 | font-size: 12px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/dashboard/shared/dashboard-button/dashboard-button.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/dashboard/shared/dashboard-button/dashboard-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | import { Speaker } from '../../../core'; 4 | 5 | @Component({ 6 | selector: 'ev-dashboard-button', 7 | templateUrl: './dashboard-button.component.html', 8 | styleUrls: ['./dashboard-button.component.css'] 9 | }) 10 | export class DashboardButtonComponent implements OnInit { 11 | @Input() speaker: Speaker; 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LoginComponent } from './login.component'; 5 | 6 | const routes: Routes = [{ path: 'login', component: LoginComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule] 11 | }) 12 | export class LoginRoutingModule {} 13 | 14 | export const routedComponents = [LoginComponent]; 15 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { map, mergeMap } from 'rxjs/operators'; 5 | 6 | import { LoginService } from './login.service'; 7 | import { ToastService, UserProfileService } from '../core'; 8 | 9 | @Component({ 10 | template: getTemplate(), 11 | providers: [LoginService] 12 | }) 13 | export class LoginComponent implements OnDestroy { 14 | private subs = new Subscription(); 15 | 16 | constructor( 17 | private loginService: LoginService, 18 | private route: ActivatedRoute, 19 | private router: Router, 20 | private toastService: ToastService, 21 | private userProfileService: UserProfileService 22 | ) {} 23 | 24 | public get isLoggedIn(): boolean { 25 | return this.userProfileService.isLoggedIn; 26 | } 27 | 28 | login() { 29 | this.subs.add( 30 | this.loginService 31 | .login() 32 | .pipe( 33 | mergeMap(loginResult => this.route.queryParams), 34 | map(qp => qp['redirectTo']) 35 | ) 36 | .subscribe(redirectTo => { 37 | this.toastService.activate(`Successfully logged in`); 38 | if (this.userProfileService.isLoggedIn) { 39 | const url = redirectTo ? [redirectTo] : ['/dashboard']; 40 | this.router.navigate(url); 41 | } 42 | }) 43 | ); 44 | } 45 | 46 | logout() { 47 | this.loginService.logout(); 48 | this.toastService.activate(`Successfully logged out`); 49 | } 50 | 51 | ngOnDestroy() { 52 | this.subs.unsubscribe(); 53 | } 54 | } 55 | 56 | function getTemplate() { 57 | return ` 58 |
59 |

Login

60 |

61 | 68 | 69 | 76 |

77 |
78 | `; 79 | } 80 | -------------------------------------------------------------------------------- /src/app/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { LoginRoutingModule, routedComponents } from './login-routing.module'; 4 | import { SharedModule } from '../shared/shared.module'; 5 | 6 | @NgModule({ 7 | imports: [LoginRoutingModule, SharedModule], 8 | declarations: [routedComponents] 9 | }) 10 | export class LoginModule {} 11 | -------------------------------------------------------------------------------- /src/app/login/login.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { of } from 'rxjs'; 3 | import { delay, tap } from 'rxjs/operators'; 4 | 5 | import { SpinnerService, UserProfileService } from '../core'; 6 | 7 | @Injectable() 8 | export class LoginService { 9 | constructor( 10 | private spinnerService: SpinnerService, 11 | private userProfileService: UserProfileService 12 | ) {} 13 | 14 | login() { 15 | return of(true).pipe( 16 | tap(_ => this.spinnerService.show()), 17 | delay(1000), 18 | tap(this.toggleLogState.bind(this)) 19 | 20 | // .do((val: boolean) => { 21 | // this.isLoggedIn = true; 22 | // console.log(this.isLoggedIn); 23 | // }); 24 | ); 25 | } 26 | 27 | logout() { 28 | this.toggleLogState(false); 29 | } 30 | 31 | private toggleLogState(val: boolean) { 32 | this.userProfileService.isLoggedIn = val; 33 | this.spinnerService.hide(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { isAuthenticatedGuard, authLoadGuard, PageNotFoundComponent } from './core'; 3 | 4 | export const routes: Routes = [ 5 | { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, 6 | { 7 | path: 'admin', 8 | loadChildren: () => import('./admin/admin.module').then((m) => m.AdminModule), 9 | canMatch: [authLoadGuard], 10 | }, 11 | { 12 | path: 'dashboard', 13 | loadChildren: () => import('./dashboard/dashboard.module').then((m) => m.DashboardModule), 14 | data: { preload: true }, 15 | }, 16 | { 17 | path: 'speakers', 18 | loadChildren: () => import('./speakers/speakers.module').then((m) => m.SpeakersModule), 19 | data: { preload: true }, 20 | }, 21 | { 22 | path: 'sessions', 23 | loadChildren: () => import('./sessions/sessions.module').then((m) => m.SessionsModule), 24 | data: { preload: true }, 25 | }, 26 | { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/app/sessions/session-list/session-list.component.css: -------------------------------------------------------------------------------- 1 | .sessions { 2 | list-style-type: none; 3 | padding: 0; 4 | } 5 | 6 | *.sessions li { 7 | padding: 4px; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/sessions/session-list/session-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Sessions

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 |
  • 12 | 13 |
  • 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/app/sessions/session-list/session-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; 2 | import { Subject, Subscription } from 'rxjs'; 3 | import { FilterTextComponent } from '../../shared/filter-text/filter-text.component'; 4 | import { FilterTextService } from '../../shared/filter-text/filter-text.service'; 5 | import { Session } from '../shared/session.model'; 6 | import { SessionService } from '../shared/session.service'; 7 | 8 | @Component({ 9 | selector: 'ev-session-list', 10 | templateUrl: './session-list.component.html', 11 | styleUrls: ['./session-list.component.css'], 12 | }) 13 | export class SessionListComponent implements OnDestroy, OnInit { 14 | private subs = new Subscription(); 15 | sessions: Session[]; 16 | filteredSessions: Session[]; 17 | 18 | @ViewChild(FilterTextComponent, { static: true }) filterComponent: FilterTextComponent; 19 | 20 | constructor(private filterService: FilterTextService, private sessionService: SessionService) { 21 | this.filteredSessions = this.sessions; 22 | } 23 | 24 | filterChanged(searchText: string) { 25 | const props = ['id', 'name', 'level']; 26 | this.filteredSessions = this.filterService.filter(searchText, props, this.sessions); 27 | } 28 | 29 | getSessions() { 30 | this.sessions = []; 31 | this.sessionService.getSessions().subscribe( 32 | (sessions) => { 33 | this.sessions = this.filteredSessions = sessions; 34 | this.filterComponent.clear(); 35 | }, 36 | (error) => { 37 | console.log('error occurred here'); 38 | console.log(error); 39 | }, 40 | () => { 41 | console.log('session retrieval completed'); 42 | } 43 | ); 44 | } 45 | 46 | ngOnDestroy() { 47 | this.subs.unsubscribe(); 48 | } 49 | 50 | ngOnInit() { 51 | componentHandler.upgradeDom(); 52 | this.getSessions(); 53 | this.subs.add(this.sessionService.onDbReset.subscribe(() => this.getSessions())); 54 | } 55 | 56 | trackBySessions(index: number, session: Session) { 57 | return session.id; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/sessions/session/session.component.css: -------------------------------------------------------------------------------- 1 | .mdl-textfield__label { 2 | top: 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/sessions/session/session.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{editSession.name | uppercase}} details

4 | 5 |
6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 18 |   19 | 20 |   21 | 22 |
23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/app/sessions/session/session.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Subject, Subscription } from 'rxjs'; 4 | 5 | import { CanComponentDeactivate, EntityService, ModalService, ToastService } from '../../core'; 6 | import { Session } from '../shared/session.model'; 7 | import { SessionService } from '../shared/session.service'; 8 | 9 | @Component({ 10 | selector: 'ev-session', 11 | templateUrl: 'session.component.html', 12 | styleUrls: ['session.component.css'], 13 | }) 14 | export class SessionComponent implements OnDestroy, OnInit, CanComponentDeactivate { 15 | private subs = new Subscription(); 16 | @Input() session: Session; 17 | editSession: Session = {}; 18 | 19 | private id: any; 20 | 21 | constructor( 22 | private entityService: EntityService, 23 | private modalService: ModalService, 24 | private route: ActivatedRoute, 25 | private router: Router, 26 | private sessionService: SessionService, 27 | private toastService: ToastService 28 | ) {} 29 | 30 | cancel(showToast = true) { 31 | this.editSession = this.entityService.clone(this.session); 32 | if (showToast) { 33 | this.toastService.activate(`Cancelled changes to ${this.session.name}`); 34 | } 35 | } 36 | 37 | canDeactivate() { 38 | const msg = 'Are you sure you want to lose your changes?'; 39 | return !this.session || !this.isDirty() || this.modalService.activate(msg); 40 | } 41 | 42 | delete() { 43 | const msg = `Do you want to delete the ${this.session.name}?`; 44 | this.modalService.activate(msg).then((responseOK) => { 45 | if (responseOK) { 46 | this.cancel(false); 47 | this.sessionService.deleteSession(this.session).subscribe( 48 | () => { 49 | // Success path 50 | this.toastService.activate(`Deleted ${this.session.name}`); 51 | this.gotoSessions(); 52 | }, 53 | (err) => this.handleServiceError('Delete', err), // Failure path 54 | () => console.log('Delete Completed') // Completed actions 55 | ); 56 | } 57 | }); 58 | } 59 | 60 | isAddMode() { 61 | return isNaN(this.id); 62 | } 63 | 64 | ngOnDestroy() { 65 | this.subs.unsubscribe(); 66 | } 67 | 68 | ngOnInit() { 69 | componentHandler.upgradeDom(); 70 | this.subs.add(this.sessionService.onDbReset.subscribe(() => this.getSession())); 71 | 72 | // ** Could use a snapshot here, as long as the parameters do not change. 73 | // ** This may happen when a component is re-used, such as fwd/back. 74 | // this.id = +this.route.snapshot.paramMap.get('id'); 75 | // 76 | // ** We could use a subscription to get the parameter, too. 77 | // ** The ActivatedRoute gets unsubscribed 78 | // this.route 79 | // .paramMap 80 | // .pipe() 81 | // map(params => params.get('id')), 82 | // tap(id => this.id = +id) 83 | // ) 84 | // .subscribe(id => this.getSession()); 85 | // 86 | // ** Instead we will use a Resolve(r) 87 | this.route.data.subscribe((data: { session: Session }) => { 88 | this.setEditSession(data.session); 89 | this.id = this.session.id; 90 | }); 91 | } 92 | 93 | save() { 94 | const session = (this.session = this.entityService.merge(this.session, this.editSession)); 95 | if (session.id == null) { 96 | this.sessionService.addSession(session).subscribe((s) => { 97 | this.setEditSession(s); 98 | this.toastService.activate(`Successfully added ${s.name}`); 99 | this.gotoSessions(); 100 | }); 101 | return; 102 | } 103 | this.sessionService 104 | .updateSession(this.session) 105 | .subscribe(() => this.toastService.activate(`Successfully saved ${this.session.name}`)); 106 | } 107 | 108 | private getSession() { 109 | if (this.id === 0) { 110 | return; 111 | } 112 | if (this.isAddMode()) { 113 | this.session = { name: '', level: '' }; 114 | this.editSession = this.entityService.clone(this.session); 115 | return; 116 | } 117 | this.sessionService 118 | .getSession(this.id) 119 | .subscribe((session: Session) => this.setEditSession(session)); 120 | } 121 | 122 | private gotoSessions() { 123 | this.router.navigate(['/sessions']); 124 | } 125 | 126 | private handleServiceError(op: string, err: any) { 127 | console.error(`${op} error: ${err.message || err}`); 128 | } 129 | 130 | private isDirty() { 131 | return this.entityService.propertiesDiffer(this.session, this.editSession); 132 | } 133 | 134 | private setEditSession(session: Session) { 135 | if (session) { 136 | this.session = session; 137 | this.editSession = this.entityService.clone(this.session); 138 | } else { 139 | this.gotoSessions(); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/app/sessions/sessions-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { SessionListComponent } from './session-list/session-list.component'; 5 | import { SessionComponent } from './session/session.component'; 6 | import { SessionsComponent } from './sessions.component'; 7 | import { SessionResolver } from './shared/session-resolver.service'; 8 | import { canDeactivateGuard, isAuthenticatedGuard } from '../core'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: SessionsComponent, 14 | canActivateChild: [isAuthenticatedGuard], 15 | children: [ 16 | { 17 | path: '', 18 | component: SessionListComponent, 19 | }, 20 | { 21 | path: ':id', 22 | component: SessionComponent, 23 | canDeactivate: [canDeactivateGuard], 24 | resolve: { 25 | session: SessionResolver, 26 | }, 27 | }, 28 | ], 29 | }, 30 | ]; 31 | 32 | @NgModule({ 33 | imports: [RouterModule.forChild(routes)], 34 | exports: [RouterModule], 35 | }) 36 | export class SessionsRoutingModule {} 37 | 38 | export const routedComponents = [SessionsComponent, SessionListComponent, SessionComponent]; 39 | -------------------------------------------------------------------------------- /src/app/sessions/sessions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: `` 5 | }) 6 | export class SessionsComponent {} 7 | -------------------------------------------------------------------------------- /src/app/sessions/sessions.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SessionButtonComponent } from './shared/session-button/session-button.component'; 4 | 5 | import { routedComponents, SessionsRoutingModule } from './sessions-routing.module'; 6 | import { SharedModule } from '../shared/shared.module'; 7 | import { SessionService } from './shared/session.service'; 8 | 9 | @NgModule({ 10 | imports: [SessionsRoutingModule, SharedModule], 11 | declarations: [SessionButtonComponent, routedComponents], 12 | 13 | // We can put this service in the component or we can do it in the module. 14 | // This module is lazy loaded, so providing a service here 15 | // allows all features in this module to use it. 16 | // providers: [SessionService] 17 | }) 18 | export class SessionsModule {} 19 | -------------------------------------------------------------------------------- /src/app/sessions/shared/session-button/session-button.component.css: -------------------------------------------------------------------------------- 1 | .mdl-card__title-text { 2 | font-size: 16px; 3 | } 4 | .mdl-card { 5 | min-height: 60px; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/sessions/shared/session-button/session-button.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{session.id}}. {{session.name | initCaps}}

4 |
5 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/sessions/shared/session-button/session-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | import { Session } from '../session.model'; 4 | 5 | @Component({ 6 | selector: 'ev-session-button', 7 | templateUrl: './session-button.component.html', 8 | styleUrls: ['./session-button.component.css'] 9 | }) 10 | export class SessionButtonComponent implements OnInit { 11 | @Input() session: Session; 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/sessions/shared/session-resolver.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | import { map, catchError } from 'rxjs/operators'; 5 | 6 | import { Session } from './session.model'; 7 | import { SessionService } from './session.service'; 8 | 9 | export class Foo { 10 | resolve( 11 | route: ActivatedRouteSnapshot, 12 | state: RouterStateSnapshot 13 | ): Session | Observable | Promise { 14 | throw new Error('Method not implemented.'); 15 | } 16 | } 17 | 18 | @Injectable({ providedIn: 'root' }) 19 | export class SessionResolver { 20 | constructor(private sessionService: SessionService, private router: Router) {} 21 | 22 | resolve( 23 | route: ActivatedRouteSnapshot, 24 | state: RouterStateSnapshot 25 | ): Session | Observable | Promise { 26 | // const id = +route.params['id']; 27 | const id = +(route.paramMap.get('id') || 0); 28 | return this.sessionService.getSession(id).pipe( 29 | map((session) => { 30 | if (session) { 31 | return session; 32 | } 33 | // Return a new object, because we're going to create a new one 34 | return new Session(); 35 | // We could throw an error here and catch it 36 | // and route back to the speaker list 37 | // let msg = `session id ${id} not found`; 38 | // console.log(msg); 39 | // throw new Error(msg) 40 | }), 41 | catchError((error: any) => { 42 | console.log(`${error}. Heading back to session list`); 43 | this.router.navigate(['/sessions']); 44 | // return of(null); 45 | return of(new Session()); 46 | }) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/sessions/shared/session.model.ts: -------------------------------------------------------------------------------- 1 | export class Session { 2 | id: number; 3 | name: string; 4 | level: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/sessions/shared/session.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { catchError, finalize, map } from 'rxjs/operators'; 5 | 6 | import { Session } from './session.model'; 7 | import { CONFIG, ExceptionService, MessageService, SpinnerService } from '../../core'; 8 | 9 | const sessionsUrl = CONFIG.baseUrls.sessions; 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class SessionService { 13 | onDbReset = this.messageService.state; 14 | 15 | private catchHttpErrors = () => (source$: Observable) => 16 | source$.pipe( 17 | catchError(this.exceptionService.catchBadResponse), 18 | finalize(() => this.spinnerService.hide()) 19 | ); 20 | 21 | constructor( 22 | private http: HttpClient, 23 | private exceptionService: ExceptionService, 24 | private messageService: MessageService, 25 | private spinnerService: SpinnerService 26 | ) { 27 | this.messageService.state.subscribe(state => this.getSessions()); 28 | } 29 | 30 | addSession(session: Session): Observable { 31 | this.spinnerService.show(); 32 | return this.http.post(`${sessionsUrl}`, session).pipe(this.catchHttpErrors()); 33 | } 34 | 35 | deleteSession(session: Session) { 36 | this.spinnerService.show(); 37 | return >( 38 | this.http.delete(`${sessionsUrl}/${session.id}`).pipe(this.catchHttpErrors()) 39 | ); 40 | } 41 | 42 | getSessions(): Observable { 43 | this.spinnerService.show(); 44 | return this.http.get(sessionsUrl).pipe( 45 | map(sessions => this.sortSessions(sessions)), 46 | this.catchHttpErrors() 47 | ); 48 | } 49 | 50 | sortSessions(sessions: Session[]) { 51 | return sessions.sort((a: Session, b: Session) => { 52 | if (a.name < b.name) { 53 | return -1; 54 | } 55 | if (a.name > b.name) { 56 | return 1; 57 | } 58 | return 0; 59 | }); 60 | } 61 | 62 | getSession(id: number): Observable { 63 | this.spinnerService.show(); 64 | return this.http.get(`${sessionsUrl}/${id}`).pipe(this.catchHttpErrors()); 65 | } 66 | 67 | updateSession(session: Session): Observable { 68 | this.spinnerService.show(); 69 | 70 | return this.http 71 | .put(`${sessionsUrl}/${session.id}`, session) 72 | .pipe(this.catchHttpErrors()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/shared/filter-text/filter-text.component.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/shared/filter-text/filter-text.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/shared/filter-text/filter-text.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ev-filter-text', 5 | templateUrl: './filter-text.component.html' 6 | }) 7 | export class FilterTextComponent { 8 | @Output() changed: EventEmitter; 9 | 10 | filter: string = ''; 11 | 12 | constructor() { 13 | this.changed = new EventEmitter(); 14 | 15 | componentHandler.upgradeDom(); 16 | } 17 | 18 | clear() { 19 | this.filter = ''; 20 | } 21 | 22 | filterChanged(event: any) { 23 | event.preventDefault(); 24 | console.log(`Filter Changed: ${this.filter}`); 25 | this.changed.emit(this.filter); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/shared/filter-text/filter-text.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { FilterTextComponent } from './filter-text.component'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, FormsModule], 9 | exports: [FilterTextComponent], 10 | declarations: [FilterTextComponent] 11 | }) 12 | export class FilterTextModule {} 13 | -------------------------------------------------------------------------------- /src/app/shared/filter-text/filter-text.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class FilterTextService { 5 | constructor() { 6 | console.log('Created an instance of FilterTextService'); 7 | } 8 | 9 | filter(data: string, props: Array, originalList: Array) { 10 | let filteredList: any[]; 11 | if (data && props && originalList) { 12 | data = data.toLowerCase(); 13 | const filtered = originalList.filter(item => { 14 | let match = false; 15 | for (const prop of props) { 16 | if ( 17 | item[prop] 18 | .toString() 19 | .toLowerCase() 20 | .indexOf(data) > -1 21 | ) { 22 | match = true; 23 | break; 24 | } 25 | } 26 | return match; 27 | }); 28 | filteredList = filtered; 29 | } else { 30 | filteredList = originalList; 31 | } 32 | return filteredList; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/shared/init-caps.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'initCaps' }) 4 | export class InitCapsPipe implements PipeTransform { 5 | transform(value: string, args?: any[]) { 6 | return value.toLowerCase().replace(/(?:^|\s)[a-z]/g, m => m.toUpperCase()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | // import { QuicklinkModule } from 'ngx-quicklink'; 5 | 6 | import { FilterTextModule } from './filter-text/filter-text.module'; 7 | 8 | import { InitCapsPipe } from './init-caps.pipe'; 9 | 10 | // imports: imports the module's exports. which are usually 11 | // declarables(components / directives / pipes) and providers. 12 | // in our case the FilterTextModule has a provider. 13 | // 14 | // exports: exports modules AND declarables (components/directives/pipes) 15 | // that other modules may want to use 16 | // SharedModule does not use CommonModule, but does use FormsModule. 17 | // Even so, we import/export both of these because most other modules 18 | // will import SharedModule and will need them. 19 | @NgModule({ 20 | imports: [ 21 | CommonModule, 22 | FilterTextModule, 23 | FormsModule 24 | // QuicklinkModule 25 | ], 26 | exports: [ 27 | CommonModule, 28 | FilterTextModule, 29 | FormsModule, 30 | // QuicklinkModule, 31 | InitCapsPipe 32 | ], 33 | declarations: [InitCapsPipe] 34 | }) 35 | export class SharedModule {} 36 | -------------------------------------------------------------------------------- /src/app/speakers/shared/speaker-button/speaker-button.component.css: -------------------------------------------------------------------------------- 1 | .mdl-card__title-text { 2 | font-size: 16px; 3 | } 4 | .mdl-card { 5 | min-height: 60px; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/speakers/shared/speaker-button/speaker-button.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{speaker.id}}. {{speaker.name}}

4 |
5 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/speakers/shared/speaker-button/speaker-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | import { Speaker } from '../../../core'; 4 | 5 | @Component({ 6 | selector: 'ev-speaker-button', 7 | templateUrl: './speaker-button.component.html', 8 | styleUrls: ['./speaker-button.component.css'] 9 | }) 10 | export class SpeakerButtonComponent implements OnInit { 11 | @Input() speaker: Speaker; 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/speakers/speaker-list/speaker-list.component.css: -------------------------------------------------------------------------------- 1 | .speakers { 2 | list-style-type: none; 3 | padding: 0; 4 | } 5 | 6 | *.speakers li { 7 | padding: 4px; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/speakers/speaker-list/speaker-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Speakers

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 |
  • 12 | 13 | 14 | 15 |
  • 16 |
17 |
{{filteredSpeakers | json}}
18 |
19 | -------------------------------------------------------------------------------- /src/app/speakers/speaker-list/speaker-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; 2 | import { Subject, Subscription } from 'rxjs'; 3 | 4 | import { Speaker, SpeakerService } from '../../core'; 5 | import { FilterTextComponent } from '../../shared/filter-text/filter-text.component'; 6 | import { FilterTextService } from '../../shared/filter-text/filter-text.service'; 7 | 8 | @Component({ 9 | selector: 'ev-speaker-list', 10 | templateUrl: './speaker-list.component.html', 11 | styleUrls: ['./speaker-list.component.css'] 12 | }) 13 | export class SpeakerListComponent implements OnDestroy, OnInit { 14 | private subs = new Subscription(); 15 | @ViewChild(FilterTextComponent, { static: true }) filterComponent: FilterTextComponent; 16 | speakers: Speaker[] = []; 17 | filteredSpeakers = this.speakers; 18 | 19 | constructor(private speakerService: SpeakerService, private filterService: FilterTextService) {} 20 | 21 | filterChanged(searchText: string) { 22 | this.filteredSpeakers = this.filterService.filter( 23 | searchText, 24 | ['id', 'name', 'twitter'], 25 | this.speakers 26 | ); 27 | } 28 | 29 | getSpeakers() { 30 | this.speakers = []; 31 | 32 | this.speakerService.getSpeakers().subscribe(speakers => { 33 | this.speakers = this.filteredSpeakers = speakers; 34 | // this.filterComponent.clear(); 35 | }); 36 | } 37 | 38 | ngOnDestroy() { 39 | this.subs.unsubscribe(); 40 | } 41 | 42 | ngOnInit() { 43 | componentHandler.upgradeDom(); 44 | this.getSpeakers(); 45 | this.subs.add(this.speakerService.onDbReset.subscribe(() => this.getSpeakers())); 46 | } 47 | 48 | trackBySpeakers(index: number, speaker: Speaker) { 49 | return speaker.id; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/speakers/speaker/speaker.component.css: -------------------------------------------------------------------------------- 1 | .mdl-textfield__label { 2 | top: 0; 3 | } 4 | 5 | .button-bar { 6 | margin: 1em 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/speakers/speaker/speaker.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{editSpeaker.name | uppercase}} details

4 | 5 |
6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 18 |   19 | 21 |   22 | 23 |
24 | 25 |
26 | 28 | 30 | 31 |
{{speaker | json}}
32 |
33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /src/app/speakers/speaker/speaker.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Observable, Subscription, of } from 'rxjs'; 4 | import { map, tap } from 'rxjs/operators'; 5 | 6 | import { Speaker, SpeakerService } from '../../core'; 7 | import { CanComponentDeactivate, EntityService, ModalService, ToastService } from '../../core'; 8 | 9 | @Component({ 10 | selector: 'ev-speaker', 11 | templateUrl: './speaker.component.html', 12 | styleUrls: ['./speaker.component.css'], 13 | }) 14 | export class SpeakerComponent implements OnDestroy, OnInit, CanComponentDeactivate { 15 | private subs = new Subscription(); 16 | @Input() speaker: Speaker; 17 | editSpeaker: Speaker = {}; 18 | showJSON = false; 19 | toggleText = 'Show JSON'; 20 | 21 | private id: any; 22 | 23 | constructor( 24 | private entityService: EntityService, 25 | private modalService: ModalService, 26 | private route: ActivatedRoute, 27 | private router: Router, 28 | private speakerService: SpeakerService, 29 | private toastService: ToastService 30 | ) {} 31 | 32 | cancel(showToast = true) { 33 | this.editSpeaker = this.entityService.clone(this.speaker); 34 | if (showToast) { 35 | this.toastService.activate(`Cancelled changes to ${this.speaker.name}`); 36 | } 37 | } 38 | 39 | canDeactivate(): Observable | Promise | boolean { 40 | if (!this.isDirty()) { 41 | return true; 42 | } 43 | 44 | const msg = 'Are you sure you want to lose your changes?'; 45 | return !this.speaker || !this.isDirty() || this.modalService.activate(msg); 46 | } 47 | 48 | delete() { 49 | const msg = `Do you want to delete ${this.speaker.name}?`; 50 | this.modalService.activate(msg).then((responseOK) => { 51 | if (responseOK) { 52 | this.cancel(false); 53 | this.speakerService.deleteSpeaker(this.speaker).subscribe( 54 | () => { 55 | this.toastService.activate(`Deleted ${this.speaker.name}`); 56 | this.gotoSpeakers(); 57 | }, 58 | (err) => this.handleServiceError('Delete', err), // Failure path 59 | () => console.log('Delete Completed') // Completed actions 60 | ); 61 | } 62 | }); 63 | } 64 | 65 | isAddMode() { 66 | return isNaN(this.id); 67 | } 68 | 69 | ngOnDestroy() { 70 | this.subs.unsubscribe(); 71 | } 72 | 73 | ngOnInit() { 74 | componentHandler.upgradeDom(); 75 | this.subs.add(this.speakerService.onDbReset.subscribe(() => this.getSpeaker())); 76 | 77 | // Could use a snapshot here, as long as the parameters do not change. 78 | // This may happen when a component is re-used. 79 | // this.id = +this.route.snapshot.paramMap.get('id'); 80 | this.route.paramMap 81 | .pipe( 82 | map((params) => params.get('id') || 0), 83 | tap((id) => (this.id = +id)) 84 | ) 85 | .subscribe((id) => this.getSpeaker()); 86 | } 87 | 88 | save() { 89 | const speaker = (this.speaker = this.entityService.merge(this.speaker, this.editSpeaker)); 90 | if (speaker.id == null) { 91 | this.speakerService.addSpeaker(speaker).subscribe((s) => { 92 | this.setEditSpeaker(s); 93 | this.toastService.activate(`Successfully added ${s.name}`); 94 | this.gotoSpeakers(); 95 | }); 96 | return; 97 | } 98 | this.speakerService 99 | .updateSpeaker(speaker) 100 | .subscribe(() => this.toastService.activate(`Successfully saved ${speaker.name}`)); 101 | } 102 | 103 | toggleJsonText() { 104 | this.showJSON = !this.showJSON; 105 | this.toggleText = this.showJSON ? 'Hide JSON' : 'Show JSON'; 106 | } 107 | 108 | private getSpeaker() { 109 | if (this.id === 0) { 110 | return; 111 | } 112 | if (this.isAddMode()) { 113 | this.speaker = { name: '', twitter: '' }; 114 | this.editSpeaker = this.entityService.clone(this.speaker); 115 | return; 116 | } 117 | this.speakerService.getSpeaker(this.id).subscribe((speaker) => this.setEditSpeaker(speaker)); 118 | } 119 | 120 | private gotoSpeakers() { 121 | this.router.navigate(['/speakers']); 122 | } 123 | 124 | private handleServiceError(op: string, err: any) { 125 | console.error(`${op} error: ${err.message || err}`); 126 | } 127 | 128 | private isDirty() { 129 | return this.entityService.propertiesDiffer(this.speaker, this.editSpeaker); 130 | } 131 | 132 | private setEditSpeaker(speaker: Speaker) { 133 | if (speaker) { 134 | this.speaker = speaker; 135 | this.editSpeaker = this.entityService.clone(this.speaker); 136 | } else { 137 | this.gotoSpeakers(); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/app/speakers/speakers-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { SpeakerListComponent } from './speaker-list/speaker-list.component'; 5 | import { SpeakerComponent } from './speaker/speaker.component'; 6 | import { SpeakersComponent } from './speakers.component'; 7 | import { canDeactivateGuard, isAuthenticatedGuard } from '../core'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: SpeakersComponent, 13 | canActivateChild: [isAuthenticatedGuard], 14 | children: [ 15 | { path: '', component: SpeakerListComponent }, 16 | { 17 | path: ':id', 18 | component: SpeakerComponent, 19 | canDeactivate: [canDeactivateGuard], 20 | }, 21 | ], 22 | }, 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forChild(routes)], 27 | exports: [RouterModule], 28 | }) 29 | export class SpeakersRoutingModule {} 30 | 31 | // This works too ... but let's be explicit, above 32 | // export const SpeakersRoutingModule = RouterModule.forChild(routes); 33 | 34 | export const routedComponents = [SpeakersComponent, SpeakerListComponent, SpeakerComponent]; 35 | -------------------------------------------------------------------------------- /src/app/speakers/speakers.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: `` 5 | }) 6 | export class SpeakersComponent {} 7 | -------------------------------------------------------------------------------- /src/app/speakers/speakers.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SpeakerButtonComponent } from './shared/speaker-button/speaker-button.component'; 4 | import { SpeakersRoutingModule, routedComponents } from './speakers-routing.module'; 5 | import { SharedModule } from '../shared/shared.module'; 6 | 7 | @NgModule({ 8 | imports: [SharedModule, SpeakersRoutingModule], 9 | declarations: [SpeakerButtonComponent, routedComponents] 10 | }) 11 | export class SpeakersModule {} 12 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/angular-event-view-cli/7d50ccf1afe29be234ece1f61d44e73834cebf9f/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/app.css: -------------------------------------------------------------------------------- 1 | /*article.template { 2 | opacity: 0; 3 | -webkit-transition: opacity 400ms ease-in; 4 | -moz-transition: opacity 400ms ease-in; 5 | transition: opacity 400ms ease-in; 6 | }*/ 7 | 8 | .animated { 9 | -webkit-animation-duration: 400ms; 10 | animation-duration: 400ms; 11 | -webkit-animation-fill-mode: both; 12 | animation-fill-mode: both; 13 | } 14 | 15 | .not-displayed { 16 | display: none; 17 | } 18 | 19 | .story-card { 20 | max-width: 100%; 21 | } 22 | 23 | @media (max-width: 320px) { 24 | .story-button { 25 | font-size: 12px; 26 | padding: 0em; 27 | } 28 | } 29 | 30 | @-webkit-keyframes slideInRight { 31 | from { 32 | -webkit-transform: translate3d(5%, 0, 0); 33 | transform: translate3d(5%, 0, 0); 34 | visibility: visible; 35 | } 36 | 37 | to { 38 | -webkit-transform: translate3d(0, 0, 0); 39 | transform: translate3d(0, 0, 0); 40 | } 41 | } 42 | 43 | @keyframes slideInRight { 44 | from { 45 | -webkit-transform: translate3d(5%, 0, 0); 46 | transform: translate3d(5%, 0, 0); 47 | visibility: visible; 48 | } 49 | 50 | to { 51 | -webkit-transform: translate3d(0, 0, 0); 52 | transform: translate3d(0, 0, 0); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/assets/ng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/angular-event-view-cli/7d50ccf1afe29be234ece1f61d44e73834cebf9f/src/assets/ng.png -------------------------------------------------------------------------------- /src/assets/sprite-av-white.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | background-image: url(sprite-av-white.png); 3 | } 4 | .icon-ic_add_to_queue_white_24dp { 5 | background-position: -32px -0px; 6 | width: 32px; 7 | height: 32px; 8 | } 9 | .icon-ic_airplay_white_24dp { 10 | background-position: -192px -0px; 11 | width: 32px; 12 | height: 32px; 13 | } 14 | .icon-ic_album_white_24dp { 15 | background-position: -0px -32px; 16 | width: 32px; 17 | height: 32px; 18 | } 19 | .icon-ic_art_track_white_24dp { 20 | background-position: -32px -32px; 21 | width: 32px; 22 | height: 32px; 23 | } 24 | .icon-ic_av_timer_white_24dp { 25 | background-position: -64px -0px; 26 | width: 32px; 27 | height: 32px; 28 | } 29 | .icon-ic_closed_caption_white_24dp { 30 | background-position: -64px -32px; 31 | width: 32px; 32 | height: 32px; 33 | } 34 | .icon-ic_equalizer_white_24dp { 35 | background-position: -0px -64px; 36 | width: 32px; 37 | height: 32px; 38 | } 39 | .icon-ic_explicit_white_24dp { 40 | background-position: -32px -64px; 41 | width: 32px; 42 | height: 32px; 43 | } 44 | .icon-ic_fast_forward_white_24dp { 45 | background-position: -64px -64px; 46 | width: 32px; 47 | height: 32px; 48 | } 49 | .icon-ic_fast_rewind_white_24dp { 50 | background-position: -96px -0px; 51 | width: 32px; 52 | height: 32px; 53 | } 54 | .icon-ic_fiber_dvr_white_24dp { 55 | background-position: -96px -32px; 56 | width: 32px; 57 | height: 32px; 58 | } 59 | .icon-ic_fiber_manual_record_white_24dp { 60 | background-position: -96px -64px; 61 | width: 32px; 62 | height: 32px; 63 | } 64 | .icon-ic_fiber_new_white_24dp { 65 | background-position: -0px -96px; 66 | width: 32px; 67 | height: 32px; 68 | } 69 | .icon-ic_fiber_pin_white_24dp { 70 | background-position: -32px -96px; 71 | width: 32px; 72 | height: 32px; 73 | } 74 | .icon-ic_fiber_smart_record_white_24dp { 75 | background-position: -64px -96px; 76 | width: 32px; 77 | height: 32px; 78 | } 79 | .icon-ic_forward_10_white_24dp { 80 | background-position: -96px -96px; 81 | width: 32px; 82 | height: 32px; 83 | } 84 | .icon-ic_forward_30_white_24dp { 85 | background-position: -128px -0px; 86 | width: 32px; 87 | height: 32px; 88 | } 89 | .icon-ic_forward_5_white_24dp { 90 | background-position: -128px -32px; 91 | width: 32px; 92 | height: 32px; 93 | } 94 | .icon-ic_games_white_24dp { 95 | background-position: -128px -64px; 96 | width: 32px; 97 | height: 32px; 98 | } 99 | .icon-ic_hd_white_24dp { 100 | background-position: -128px -96px; 101 | width: 32px; 102 | height: 32px; 103 | } 104 | .icon-ic_hearing_white_24dp { 105 | background-position: -0px -128px; 106 | width: 32px; 107 | height: 32px; 108 | } 109 | .icon-ic_high_quality_white_24dp { 110 | background-position: -32px -128px; 111 | width: 32px; 112 | height: 32px; 113 | } 114 | .icon-ic_library_add_white_24dp { 115 | background-position: -64px -128px; 116 | width: 32px; 117 | height: 32px; 118 | } 119 | .icon-ic_library_books_white_24dp { 120 | background-position: -96px -128px; 121 | width: 32px; 122 | height: 32px; 123 | } 124 | .icon-ic_library_music_white_24dp { 125 | background-position: -128px -128px; 126 | width: 32px; 127 | height: 32px; 128 | } 129 | .icon-ic_loop_white_24dp { 130 | background-position: -160px -0px; 131 | width: 32px; 132 | height: 32px; 133 | } 134 | .icon-ic_mic_none_white_24dp { 135 | background-position: -160px -32px; 136 | width: 32px; 137 | height: 32px; 138 | } 139 | .icon-ic_mic_off_white_24dp { 140 | background-position: -160px -64px; 141 | width: 32px; 142 | height: 32px; 143 | } 144 | .icon-ic_mic_white_24dp { 145 | background-position: -160px -96px; 146 | width: 32px; 147 | height: 32px; 148 | } 149 | .icon-ic_movie_white_24dp { 150 | background-position: -160px -128px; 151 | width: 32px; 152 | height: 32px; 153 | } 154 | .icon-ic_music_video_white_24dp { 155 | background-position: -0px -160px; 156 | width: 32px; 157 | height: 32px; 158 | } 159 | .icon-ic_new_releases_white_24dp { 160 | background-position: -32px -160px; 161 | width: 32px; 162 | height: 32px; 163 | } 164 | .icon-ic_not_interested_white_24dp { 165 | background-position: -64px -160px; 166 | width: 32px; 167 | height: 32px; 168 | } 169 | .icon-ic_pause_circle_filled_white_24dp { 170 | background-position: -96px -160px; 171 | width: 32px; 172 | height: 32px; 173 | } 174 | .icon-ic_pause_circle_outline_white_24dp { 175 | background-position: -128px -160px; 176 | width: 32px; 177 | height: 32px; 178 | } 179 | .icon-ic_pause_white_24dp { 180 | background-position: -160px -160px; 181 | width: 32px; 182 | height: 32px; 183 | } 184 | .icon-ic_play_arrow_white_24dp { 185 | background-position: -0px -0px; 186 | width: 32px; 187 | height: 32px; 188 | } 189 | .icon-ic_play_circle_filled_white_24dp { 190 | background-position: -192px -32px; 191 | width: 32px; 192 | height: 32px; 193 | } 194 | .icon-ic_play_circle_outline_white_24dp { 195 | background-position: -192px -64px; 196 | width: 32px; 197 | height: 32px; 198 | } 199 | .icon-ic_playlist_add_check_white_24dp { 200 | background-position: -192px -96px; 201 | width: 32px; 202 | height: 32px; 203 | } 204 | .icon-ic_playlist_add_white_24dp { 205 | background-position: -192px -128px; 206 | width: 32px; 207 | height: 32px; 208 | } 209 | .icon-ic_playlist_play_white_24dp { 210 | background-position: -192px -160px; 211 | width: 32px; 212 | height: 32px; 213 | } 214 | .icon-ic_queue_music_white_24dp { 215 | background-position: -0px -192px; 216 | width: 32px; 217 | height: 32px; 218 | } 219 | .icon-ic_queue_play_next_white_24dp { 220 | background-position: -32px -192px; 221 | width: 32px; 222 | height: 32px; 223 | } 224 | .icon-ic_queue_white_24dp { 225 | background-position: -64px -192px; 226 | width: 32px; 227 | height: 32px; 228 | } 229 | .icon-ic_radio_white_24dp { 230 | background-position: -96px -192px; 231 | width: 32px; 232 | height: 32px; 233 | } 234 | .icon-ic_recent_actors_white_24dp { 235 | background-position: -128px -192px; 236 | width: 32px; 237 | height: 32px; 238 | } 239 | .icon-ic_remove_from_queue_white_24dp { 240 | background-position: -160px -192px; 241 | width: 32px; 242 | height: 32px; 243 | } 244 | .icon-ic_repeat_one_white_24dp { 245 | background-position: -192px -192px; 246 | width: 32px; 247 | height: 32px; 248 | } 249 | .icon-ic_repeat_white_24dp { 250 | background-position: -224px -0px; 251 | width: 32px; 252 | height: 32px; 253 | } 254 | .icon-ic_replay_10_white_24dp { 255 | background-position: -224px -32px; 256 | width: 32px; 257 | height: 32px; 258 | } 259 | .icon-ic_replay_30_white_24dp { 260 | background-position: -224px -64px; 261 | width: 32px; 262 | height: 32px; 263 | } 264 | .icon-ic_replay_5_white_24dp { 265 | background-position: -224px -96px; 266 | width: 32px; 267 | height: 32px; 268 | } 269 | .icon-ic_replay_white_24dp { 270 | background-position: -224px -128px; 271 | width: 32px; 272 | height: 32px; 273 | } 274 | .icon-ic_shuffle_white_24dp { 275 | background-position: -224px -160px; 276 | width: 32px; 277 | height: 32px; 278 | } 279 | .icon-ic_skip_next_white_24dp { 280 | background-position: -224px -192px; 281 | width: 32px; 282 | height: 32px; 283 | } 284 | .icon-ic_skip_previous_white_24dp { 285 | background-position: -0px -224px; 286 | width: 32px; 287 | height: 32px; 288 | } 289 | .icon-ic_slow_motion_video_white_24dp { 290 | background-position: -32px -224px; 291 | width: 32px; 292 | height: 32px; 293 | } 294 | .icon-ic_snooze_white_24dp { 295 | background-position: -64px -224px; 296 | width: 32px; 297 | height: 32px; 298 | } 299 | .icon-ic_sort_by_alpha_white_24dp { 300 | background-position: -96px -224px; 301 | width: 32px; 302 | height: 32px; 303 | } 304 | .icon-ic_stop_white_24dp { 305 | background-position: -128px -224px; 306 | width: 32px; 307 | height: 32px; 308 | } 309 | .icon-ic_subscriptions_white_24dp { 310 | background-position: -160px -224px; 311 | width: 32px; 312 | height: 32px; 313 | } 314 | .icon-ic_subtitles_white_24dp { 315 | background-position: -192px -224px; 316 | width: 32px; 317 | height: 32px; 318 | } 319 | .icon-ic_surround_sound_white_24dp { 320 | background-position: -224px -224px; 321 | width: 32px; 322 | height: 32px; 323 | } 324 | .icon-ic_video_library_white_24dp { 325 | background-position: -256px -0px; 326 | width: 32px; 327 | height: 32px; 328 | } 329 | .icon-ic_videocam_off_white_24dp { 330 | background-position: -256px -32px; 331 | width: 32px; 332 | height: 32px; 333 | } 334 | .icon-ic_videocam_white_24dp { 335 | background-position: -256px -64px; 336 | width: 32px; 337 | height: 32px; 338 | } 339 | .icon-ic_volume_down_white_24dp { 340 | background-position: -256px -96px; 341 | width: 32px; 342 | height: 32px; 343 | } 344 | .icon-ic_volume_mute_white_24dp { 345 | background-position: -256px -128px; 346 | width: 32px; 347 | height: 32px; 348 | } 349 | .icon-ic_volume_off_white_24dp { 350 | background-position: -256px -160px; 351 | width: 32px; 352 | height: 32px; 353 | } 354 | .icon-ic_volume_up_white_24dp { 355 | background-position: -256px -192px; 356 | width: 32px; 357 | height: 32px; 358 | } 359 | .icon-ic_web_asset_white_24dp { 360 | background-position: -256px -224px; 361 | width: 32px; 362 | height: 32px; 363 | } 364 | .icon-ic_web_white_24dp { 365 | background-position: -0px -256px; 366 | width: 32px; 367 | height: 32px; 368 | } 369 | -------------------------------------------------------------------------------- /src/assets/sprite-av-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/angular-event-view-cli/7d50ccf1afe29be234ece1f61d44e73834cebf9f/src/assets/sprite-av-white.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/angular-event-view-cli/7d50ccf1afe29be234ece1f61d44e73834cebf9f/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EventViewCli 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { environment } from './environments/environment'; 4 | import { AppModule } from './app/app.module'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic().bootstrapModule(AppModule); 11 | -------------------------------------------------------------------------------- /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 recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /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/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 12 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // // Manually add stuff below here 2 | // declare var componentHandler: any; 3 | declare var componentHandler: any; 4 | 5 | /* SystemJS module definition */ 6 | declare var module: NodeModule; 7 | interface NodeModule { 8 | id: string; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "downlevelIteration": true, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "es2020", 22 | "lib": [ 23 | "es2020", 24 | "dom" 25 | ], 26 | "strictPropertyInitialization": false, 27 | "strictFunctionTypes": false, 28 | "useDefineForClassFields": false 29 | }, 30 | "angularCompilerOptions": { 31 | "enableI18nLegacyMessageIdFormat": false, 32 | "strictInjectionParameters": true, 33 | "strictInputAccessModifiers": true, 34 | "strictTemplates": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "eofline": true, 13 | "forin": true, 14 | "import-blacklist": [true], 15 | "import-spacing": true, 16 | "indent": [true, "spaces"], 17 | "interface-over-type-literal": true, 18 | "label-position": true, 19 | "max-line-length": [true, 140], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | { 24 | "order": [ 25 | "static-field", 26 | "instance-field", 27 | "static-method", 28 | "instance-method" 29 | ] 30 | } 31 | ], 32 | "no-arg": true, 33 | "no-bitwise": true, 34 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 35 | "no-construct": true, 36 | "no-debugger": true, 37 | "no-duplicate-super": true, 38 | "no-empty": false, 39 | "no-empty-interface": true, 40 | "no-eval": true, 41 | "no-inferrable-types": [true, "ignore-params"], 42 | "no-misused-new": true, 43 | "no-non-null-assertion": true, 44 | "no-redundant-jsdoc": true, 45 | "no-shadowed-variable": true, 46 | "no-string-literal": false, 47 | "no-string-throw": true, 48 | "no-switch-case-fall-through": true, 49 | "no-trailing-whitespace": true, 50 | "no-unnecessary-initializer": true, 51 | "no-unused-expression": true, 52 | "no-var-keyword": true, 53 | "object-literal-sort-keys": false, 54 | "one-line": [ 55 | true, 56 | "check-open-brace", 57 | "check-catch", 58 | "check-else", 59 | "check-whitespace" 60 | ], 61 | "prefer-const": true, 62 | "quotemark": [true, "single"], 63 | "radix": true, 64 | "semicolon": [true, "always"], 65 | "triple-equals": [true, "allow-null-check"], 66 | "typedef-whitespace": [ 67 | true, 68 | { 69 | "call-signature": "nospace", 70 | "index-signature": "nospace", 71 | "parameter": "nospace", 72 | "property-declaration": "nospace", 73 | "variable-declaration": "nospace" 74 | } 75 | ], 76 | "unified-signatures": true, 77 | "variable-name": false, 78 | "whitespace": [ 79 | true, 80 | "check-branch", 81 | "check-decl", 82 | "check-operator", 83 | "check-separator", 84 | "check-type" 85 | ], 86 | "no-output-on-prefix": true, 87 | "no-inputs-metadata-property": true, 88 | "no-outputs-metadata-property": true, 89 | "no-host-metadata-property": true, 90 | "no-input-rename": true, 91 | "no-output-rename": true, 92 | "use-lifecycle-interface": true, 93 | "use-pipe-transform-interface": true, 94 | "component-class-suffix": true, 95 | "directive-class-suffix": true 96 | } 97 | } 98 | --------------------------------------------------------------------------------