├── .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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------