├── .angular-cli.json
├── .dockerignore
├── .editorconfig
├── .gitignore
├── .vscode
└── launch.json
├── Dockerfile
├── README.md
├── RESOURCES.md
├── e2e
├── app.e2e-spec.ts
├── app.po.ts
└── tsconfig.e2e.json
├── generate-sw.js
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── proxy.conf.json
├── src
├── client
│ ├── app
│ │ ├── app-routing.module.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── heroes
│ │ │ ├── hero-list.component.ts
│ │ │ ├── heroes-routing.module.ts
│ │ │ └── heroes.module.ts
│ │ ├── home.component.ts
│ │ ├── page-not-found.component.ts
│ │ ├── push.component.ts
│ │ ├── sync.component.ts
│ │ └── villains
│ │ │ ├── villain-list.component.ts
│ │ │ ├── villains-routing.module.ts
│ │ │ └── villains.module.ts
│ ├── assets
│ │ ├── .gitkeep
│ │ ├── idb-keyval-min.js
│ │ ├── launcher-icon-2x.jpg
│ │ ├── launcher-icon-3x.jpg
│ │ ├── launcher-icon-4x.jpg
│ │ └── ng.png
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── manifest.json
│ ├── offline.html
│ ├── polyfills.ts
│ ├── styles
│ │ ├── mixin.scss
│ │ ├── styles.scss
│ │ └── theme.scss
│ ├── sw.js
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── typings.d.ts
└── server
│ ├── api
│ ├── heroes.json
│ └── villains.json
│ ├── index.js
│ ├── package.json
│ └── routes.js
├── tsconfig.json
└── tslint.json
/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "pwa-angular"
5 | },
6 | "apps": [
7 | {
8 | "root": "src/client",
9 | "outDir": "dist/public",
10 | "assets": [
11 | "assets",
12 | "favicon.ico",
13 | "manifest.json",
14 | "sw.js",
15 | "offline.html",
16 | {
17 | "glob": "**/*.*",
18 | "input": "../server/",
19 | "output": "../"
20 | },
21 | {
22 | "glob": "workbox-sw.prod.*.js",
23 | "input": "../../node_modules/workbox-sw/build/importScripts/",
24 | "output": "./"
25 | }
26 | ],
27 | "index": "index.html",
28 | "main": "main.ts",
29 | "polyfills": "polyfills.ts",
30 | "test": "test.ts",
31 | "tsconfig": "tsconfig.app.json",
32 | "testTsconfig": "tsconfig.spec.json",
33 | "prefix": "pwa",
34 | "styles": ["styles/theme.scss", "styles/mixin.scss", "styles/styles.scss"],
35 | "scripts": [],
36 | "environmentSource": "environments/environment.ts",
37 | "environments": {
38 | "dev": "environments/environment.ts",
39 | "prod": "environments/environment.prod.ts"
40 | }
41 | }
42 | ],
43 | "e2e": {
44 | "protractor": {
45 | "config": "./protractor.conf.js"
46 | }
47 | },
48 | "lint": [
49 | {
50 | "project": "src/client/tsconfig.app.json"
51 | },
52 | {
53 | "project": "src/client/tsconfig.spec.json"
54 | },
55 | {
56 | "project": "e2e/tsconfig.e2e.json"
57 | }
58 | ],
59 | "test": {
60 | "karma": {
61 | "config": "./karma.conf.js"
62 | }
63 | },
64 | "defaults": {
65 | "styleExt": "scss",
66 | "component": {
67 | "flat": true,
68 | "spec": false,
69 | "inlineStyle": true,
70 | "inlineTemplate": true
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/.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
11 |
12 | dist/node_modules
13 | dist/npm-debug.log
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | testem.log
34 | /typings
35 |
36 | # e2e
37 | /e2e/*.js
38 | /e2e/*.map
39 |
40 | # System Files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "attach",
10 | "name": "Attach",
11 | "port": 5858
12 | },
13 | {
14 | "type": "node",
15 | "request": "launch",
16 | "name": "Launch Program",
17 | "program": "${file}"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/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 | WORKDIR /app
6 | COPY package.json /app
7 | RUN npm install
8 | COPY . /app
9 | RUN ng build --prod
10 | # Generate the PWA's Service Worker
11 | RUN npm run generate-sw
12 |
13 | #Express server =======================================
14 | FROM node:6.11-alpine as express-server
15 | WORKDIR /app
16 | COPY /src/server /app
17 | RUN npm install --production --silent
18 |
19 | #Final image ========================================
20 | FROM node:6.11-alpine
21 | RUN mkdir -p /usr/src/app
22 | WORKDIR /usr/src/app
23 | COPY --from=express-server /app /usr/src/app
24 | COPY --from=angular-app /app/dist /usr/src/app
25 | ENV PORT 80
26 | CMD [ "node", "index.js" ]
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PwaAngular
2 |
3 | ## Try It
4 |
5 | Run the master branch example:
6 |
7 | ```bash
8 | # git checkout [branch-name]
9 | npm i
10 |
11 | # build prod build, generate sw, run the express server out of /dist
12 | npm start
13 | ```
14 |
15 | Or run it in dev mode:
16 |
17 | ```bash
18 | npm i
19 |
20 | # start the Node express API server in dev, and the angular app, but no SW
21 | npm run start-dev-proxy
22 | ```
23 |
24 |
25 | > Enable the twilio texting feature by creating a Twilio account and running
26 | `TWILIO_ACCOUNT_SID="sid-goes-here" TWILIO_AUTH_TOKEN="auth-token" TWILIO_PHONE="from-phone" node --inspect=5858 index.js`
27 | > Push notifications require a push server
28 |
29 | ## Variations
30 |
31 | There are various techniques in this repo, contained in different branches. Each accomplishes similar tasks, with variations n the end result. Learn moer below. All contain `manifest.json`, and variations on the service worker and code to use the servie worker.
32 |
33 | | branch | features |
34 | |---|---|
35 | | sw/online-only | no PWA qualities |
36 | | sw/workbox/generate | Generates a service worker with caching, no custom code |
37 | | sw/manual | All custom code for caching, sync, and push |
38 | | sw/workbox/inject-cache-and-extend | Workbox to cache, custom code for sync and push |
39 | | master | same as sw/workbox/inject-cache-and-extend |
40 | | ng | wip |
41 |
42 | ### Online only
43 |
44 | - *sw/online-only* branch
45 |
46 | The app only runs online. There is no service worker `sw.js` and no `manifest.json`. Push notifications are disabled.
47 |
48 | ### Workbox Caching by Generating the Entire Service Worker
49 |
50 | - *sw/workbox/generate* branch
51 |
52 | The generated service worker contains all of the logic that to accomplish:
53 |
54 | 1. precache of app shell files
55 | 1. fallback to `/offline.html` for unknown routes
56 | 1. runtime caching of routes (e.g. `/api/data` routes)
57 |
58 | #### Precaching
59 |
60 | The precache list is generated along with the service worker. The first time the app loads, a list of files retrieved over the network are matched against the list of precache routes. The matches' responses are stored in Application Cache. Subequent page refreshes will check the cache first for a match, if found it will return the reponse.
61 |
62 | After responding, another fetch is fired off to check if the response has changed. If the response changed, it will cache the new response for next time.
63 |
64 | #### Runtime caching
65 |
66 | When a route cannot be found in cache, it is fetched. If the fetch returns a valid response, it is added to the Application Cache. Data calls to the api will be cached with runtime caching
67 |
68 | After responding, another fetch is fired off to check if the response has changed. If the response changed, it will cache the new response for next time.
69 |
70 | #### Fallback
71 |
72 | When a route cannot be found in cache nor via fetch, the app redirects to an offline.html page.
73 |
74 | ### Manual Service Worker
75 |
76 | - *sw/manual* branch
77 |
78 | The manual service worker contains a `sw.js` file which contains all of the logic that to accomplish:
79 | 1. precache of app shell files
80 | 2. fallback to `/offline.html` for unknown routes
81 | 3. runtime caching of routes (e.g. `/api/data` routes)
82 | 4. background sync
83 | 5. push notification subscription
84 |
85 | #### Precaching
86 |
87 | The precache list is stored in a `manifest.js` file that is generated by *workbox-build* process. The `sw.js` reads the `manifest.js` to get the precache list of files. The first time the app loads, a list of files retrieved over the network are matched against the list of precache routes. The matches' responses are stored in Application Cache. Subequent page refreshes will check the cache first for a match, if found it will return the reponse.
88 |
89 | After responding, another fetch is fired off to check if the response has changed. If the response changed, it will cache the new response for next time.
90 |
91 | #### Runtime caching
92 |
93 | When a route cannot be found in cache, it is fetched. If the fetch returns a valid response, it is added to the Application Cache. Data calls to the api will be cached with runtime caching
94 |
95 | After responding, another fetch is fired off to check if the response has changed. If the response changed, it will cache the new response for next time.
96 |
97 | #### Fallback
98 |
99 | When a route cannot be found in cache nor via fetch, the app redirects to an offline.html page.
100 |
101 | #### Background Sync
102 |
103 | When the user attempts to send a text message, the app detects if the service worker is available. If the service worker is available, the message is stored in IndexDB and a background sync message is sent to let the service worker know that message is ready. The service worker listens for the sync event and for the specific message tag. It then retrieves the message(s) from IndexDB and sends the text message(s) to the server. If the http post was successful, the messages are removed from IndexDB. If the http post fails, the messages remain in IndexDb.
104 |
105 | If service worker is not available, the messages are sent online directly from the app, with no service worker involvement.
106 |
107 | When the app is offline, the service worker's sync event does not fire. Once the app goes back online, the sync event fires and all messages found in IndexDB are posted to the server.
108 |
109 | #### Push Notification
110 | When the user clicks the "subscribe" button, the service worker will subscribe to a specific push notification from the server using a key. From this point when the server sends a push notification, the service worker will listen for it and show the push notification. Once the user unsubscribes, the push notifications will cease.
111 |
112 | ### Workbox Caching and Extend with Manual Background Sync and Push
113 |
114 | - *sw/workbox/inject-cache-and-extend* branch
115 |
116 | The manual service worker contains a `sw.js` file which contains all of the logic that to accomplish:
117 |
118 | 1. precache of app shell files
119 | 1. fallback to `/offline.html` for unknown routes
120 | 1. runtime caching of routes (e.g. `/api/data` routes)
121 | 1. background sync
122 | 1. push notification subscription
123 |
124 | #### Precaching
125 |
126 | The precache list is generated by *workbox-build* process and injected into the `sw.js` file. The first time the app loads, a list of files retrieved over the network are matched against the list of precache routes. The matches' responses are stored in Application Cache. Subequent page refreshes will check the cache first for a match, if found it will return the reponse.
127 |
128 | After responding, another fetch is fired off to check if the response has changed. If the response changed, it will cache the new response for next time.
129 |
130 | #### Runtime caching
131 |
132 | When a route cannot be found in cache, it is fetched. If the fetch returns a valid response, it is added to the Application Cache. Data calls to the api will be cached with runtime caching
133 |
134 | After responding, another fetch is fired off to check if the response has changed. If the response changed, it will cache the new response for next time.
135 |
136 | #### Fallback
137 |
138 | When a route cannot be found in cache nor via fetch, the app redirects to an offline.html page.
139 |
140 | #### Background Sync
141 |
142 | When the user attempts to send a text message, the app detects if the service worker is available. If the service worker is available, the message is stored in IndexDB and a background sync message is sent to let the service worker know that message is ready. The service worker listens for the sync event and for the specific message tag. It then retrieves the message(s) from IndexDB and sends the text message(s) to the server. If the http post was successful, the messages are removed from IndexDB. If the http post fails, the messages remain in IndexDb.
143 |
144 | If service worker is not available, the messages are sent online directly from the app, with no service worker involvement.
145 |
146 | When the app is offline, the service worker's sync event does not fire. Once the app goes back online, the sync event fires and all messages found in IndexDB are posted to the server.
147 |
148 | #### Push Notification
149 |
150 | When the user clicks the "subscribe" button, the service worker will subscribe to a specific push notification from the server using a key. From this point when the server sends a push notification, the service worker will listen for it and show the push notification. Once the user unsubscribes, the push notifications will cease.
151 |
--------------------------------------------------------------------------------
/RESOURCES.md:
--------------------------------------------------------------------------------
1 | ## What Can PWAs Do?
2 |
3 | When launched from the user’s home screen, service workers enable a Progressive Web App to load instantly, regardless of the network state.
4 |
5 | - look and feel like an app
6 | - launch instantly
7 | - send push notifications
8 | - work offline
9 | - icon on the homescreen
10 |
11 |
12 | ## Technology
13 |
14 |
15 | ### App Shell
16 |
17 | The App Shell design approach enables the initial load of a mobile web app to provide a basic shell of a app UI, and the content for the app is loaded after. The App Shell is cached separate from our main app, to enable the shell to load quickly.
18 |
19 |
20 | ### Meta Tags
21 |
22 | Links
23 | - [generator icons and meta tags](http://realfavicongenerator.net/)
24 |
25 | ### App Manifest
26 |
27 | We write a manifest.json file that helps enable features.
28 |
29 | - icons
30 | - splash screen
31 | - theme colors
32 | - the URL that's opened.
33 |
34 | Currently, iOS doesn’t have any additional features here beyond Pin to Homescreen. Chrome on Android added support for installing web apps to the homescreen with a native install banner.
35 |
36 | Links
37 | - [App Manifest generator](https://app-manifest.firebaseapp.com/)
38 | - https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
39 | - https://manifest-validator.appspot.com/
40 |
41 | Support
42 | - http://caniuse.com/#feat=web-app-manifest
43 |
44 |
45 | ### Service Workers
46 |
47 | Service Worker is a worker script that runs in response to events like network requests, push notifications, connectivity changes in the background. A service worker puts you in control of the cache and how to respond to resource requests.
48 |
49 | They Power:
50 | - offline functionality
51 | - push notifications
52 | - background content updating
53 | - content caching
54 |
55 | Links:
56 | - https://serviceworke.rs/
57 | - https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker
58 | - https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
59 | - https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle
60 | - https://jakearchibald.github.io/isserviceworkerready/
61 | - https://w3c.github.io/manifest/#serviceworker-member
62 | - https://coryrylan.com/blog/fast-offline-angular-apps-with-service-workers
63 | - https://serviceworke.rs/caching-strategies.html
64 | - https://developers.google.com/web/tools/workbox/
65 | - https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
66 | - https://bitsofco.de/the-service-worker-lifecycle/
67 |
68 | Support
69 | http://caniuse.com/#feat=serviceworkers
70 |
71 |
72 | ### Background Sync
73 |
74 | The service worker can send messages in the background.
75 |
76 | They Power:
77 | - ability to unblock the user when they want to send data over the network
78 |
79 | Links:
80 | - https://developers.google.com/web/updates/2015/12/background-sync
81 | - https://ponyfoo.com/articles/backgroundsync
82 | - https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts
83 | - https://www.twilio.com/blog/2017/02/send-messages-when-youre-back-online-with-service-workers-and-background-sync.html
84 |
85 |
86 | ### Push
87 |
88 | The service worker can listen for push notifications.
89 |
90 | They Power:
91 | - ability to receive and act upon push notifications in the web
92 | - subscribe and unsbuscribe to the push notifications' server
93 |
94 | Links:
95 | - https://serviceworke.rs/push-simple.html
96 | - https://developers.google.com/web/fundamentals/getting-started/codelabs/push-notifications/
97 | - https://github.com/GoogleChrome/push-notifications
98 | - https://github.com/web-push-libs/web-push
99 | - https://developers.google.com/web/ilt/pwa/lab-integrating-web-push
100 | - https://web-push-codelab.appspot.com/
101 |
102 |
103 | ### PWA Resources
104 |
105 | - https://serviceworke.rs/
106 | - https://workboxjs.org/
107 | - https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/
108 | - http://www.techrepublic.com/article/why-its-time-for-businesses-to-get-serious-about-progressive-web-apps
109 | - http://www.cmo.com/features/articles/2017/4/27/get-ready-to-surf-the-next-wave-of-the-mobile-webpwas.html
110 | - http://blog.ionic.io/what-is-a-progressive-web-app/
111 | - https://developers.google.com/web/progressive-web-apps/
112 | - https://developers.google.com/web/progressive-web-apps/checklist
113 | - http://www.androidcentral.com/twitter-lite-progressive-web-app-thats-designed-emerging-markets
114 | - https://github.com/GoogleChrome/samples/blob/gh-pages/web-application-manifest/manifest.json
115 | - https://jakearchibald.com/2014/offline-cookbook/
116 | - https://medium.com/javascript-scene/native-apps-are-doomed-ac397148a2c0
117 | - https://www.talater.com/upup/
118 | - https://addyosmani.com/blog/getting-started-with-progressive-web-apps/
119 | - http://bit.ly/pwa-angularsummit-2017
120 | - http://slides.com/webmax/pwa-ngpoland#/33
121 | - https://docs.google.com/document/d/19S5ozevWighny788nI99worpcIMDnwWVmaJDGf_RoDY/edit#heading=h.z3fi3lc8mayc
122 |
123 | ### PWA Videos
124 | - https://www.youtube.com/watch?v=cmGr0RszHc8
125 | - https://www.udacity.com/course/offline-web-applications--ud899
126 | - https://www.youtube.com/watch?v=C8KcW1Nj3Mw
127 |
128 |
129 | ### Samples
130 |
131 | - https://paperplanes.world/
132 | - https://github.com/webmaxru/pwa-workshop-angular/tree/step9
133 | - https://pwa-workshop-angular.firebaseapp.com/
134 |
135 |
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { PwaAngularPage } from './app.po';
2 |
3 | describe('pwa-angular App', () => {
4 | let page: PwaAngularPage;
5 |
6 | beforeEach(() => {
7 | page = new PwaAngularPage();
8 | });
9 |
10 | it('should display message saying app works', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('pwa works!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class PwaAngularPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('pwa-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types": [
8 | "jasmine",
9 | "node"
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/generate-sw.js:
--------------------------------------------------------------------------------
1 | const workboxBuild = require('workbox-build');
2 | const SRC_DIR = 'src/client';
3 | const BUILD_DIR = 'dist/public';
4 | const SW = 'sw.js';
5 | const globPatterns = [
6 | '**/*.{js,png,ico,svg,html,css}',
7 | 'assets/**/*'
8 | ];
9 |
10 | const globIgnores = [
11 | 'package.json',
12 | 'index.js',
13 | 'sw.js'
14 | ];
15 |
16 | const input = {
17 | swSrc: `${SRC_DIR}/${SW}`,
18 | swDest: `${BUILD_DIR}/${SW}`,
19 | globDirectory: BUILD_DIR,
20 | globPatterns: globPatterns,
21 | globIgnores: globIgnores,
22 | maximumFileSizeToCacheInBytes: 4000000
23 | };
24 |
25 | workboxBuild.injectManifest(input).then(() => {
26 | console.log(`The service worker ${BUILD_DIR}/${SW} has been injected with a precache list.`);
27 | });
28 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | files: [
19 | { pattern: './src/client/test.ts', watched: false }
20 | ],
21 | preprocessors: {
22 | './src/client/test.ts': ['@angular/cli']
23 | },
24 | mime: {
25 | 'text/x-typescript': ['ts','tsx']
26 | },
27 | coverageIstanbulReporter: {
28 | 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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pwa-angular",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng build --prod && npm run generate-sw && cd dist && node index.js",
8 | "start-dev": "ng build && npm run generate-sw && cd dist && node index.js",
9 | "start-dev-proxy": "concurrently \"PORT=4201 node src/server/index.js\" \"ng serve --proxy-config proxy.conf.json\"",
10 | "build": "ng build",
11 | "test": "ng test",
12 | "lint": "ng lint",
13 | "e2e": "ng e2e",
14 | "generate-sw": "node generate-sw.js"
15 | },
16 | "engines": {
17 | "node": "6.9.1"
18 | },
19 | "private": true,
20 | "dependencies": {
21 | "@angular/animations": "^4.3.0",
22 | "@angular/cdk": "^2.0.0-beta.10",
23 | "@angular/common": "^4.3.0",
24 | "@angular/compiler": "^4.3.0",
25 | "@angular/core": "^4.3.0",
26 | "@angular/forms": "^4.3.0",
27 | "@angular/http": "^4.3.0",
28 | "@angular/material": "^2.0.0-beta.10",
29 | "@angular/platform-browser": "^4.3.0",
30 | "@angular/platform-browser-dynamic": "^4.3.0",
31 | "@angular/router": "^4.3.0",
32 | "body-parser": "^1.17.2",
33 | "core-js": "^2.4.1",
34 | "express": "^4.15.3",
35 | "idb-keyval": "^2.3.0",
36 | "rxjs": "^5.4.2",
37 | "twilio": "^3.3.0",
38 | "workbox-build": "2.0.0",
39 | "workbox-sw": "2.0.0",
40 | "zone.js": "^0.8.14"
41 | },
42 | "devDependencies": {
43 | "@angular/cli": "^1.3.2",
44 | "@angular/compiler-cli": "^4.2.4",
45 | "@angular/language-service": "^4.2.4",
46 | "@types/jasmine": "~2.5.53",
47 | "@types/jasminewd2": "~2.0.2",
48 | "@types/node": "~6.0.60",
49 | "codelyzer": "~3.1.1",
50 | "concurrently": "^3.5.0",
51 | "jasmine-core": "~2.6.2",
52 | "jasmine-spec-reporter": "~4.1.0",
53 | "karma": "~1.7.0",
54 | "karma-chrome-launcher": "~2.1.1",
55 | "karma-cli": "~1.0.1",
56 | "karma-coverage-istanbul-reporter": "^1.2.1",
57 | "karma-jasmine": "~1.1.0",
58 | "karma-jasmine-html-reporter": "^0.2.2",
59 | "protractor": "~5.1.2",
60 | "ts-node": "~3.2.0",
61 | "tslint": "~5.3.2",
62 | "typescript": "~2.3.3"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://localhost:4201",
4 | "secure": false
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/client/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
3 | import { HomeComponent } from './home.component';
4 | import { PageNotFoundComponent } from './page-not-found.component';
5 |
6 | const routes: Routes = [
7 | { path: '', pathMatch: 'full', redirectTo: 'home' },
8 | { path: 'home', component: HomeComponent },
9 | { path: 'heroes', loadChildren: 'app/heroes/heroes.module#HeroesModule' },
10 | {
11 | path: 'villains',
12 | loadChildren: 'app/villains/villains.module#VillainsModule'
13 | },
14 | { path: '**', pathMatch: 'full', component: PageNotFoundComponent }
15 | ];
16 |
17 | @NgModule({
18 | imports: [
19 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
20 | ],
21 | exports: [RouterModule]
22 | })
23 | export class AppRoutingModule {}
24 |
--------------------------------------------------------------------------------
/src/client/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'pwa-root',
5 | template: `
6 |
7 |
19 |
20 |
21 | `,
22 | styles: []
23 | })
24 | export class AppComponent {
25 | title = 'pwa works!';
26 | }
27 |
--------------------------------------------------------------------------------
/src/client/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 | import { HttpClientModule } from '@angular/common/http';
4 | import { MdButtonModule, MdInputModule, MdToolbarModule } from '@angular/material';
5 | import { BrowserModule } from '@angular/platform-browser';
6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
7 |
8 | import { AppRoutingModule } from './app-routing.module';
9 | import { AppComponent } from './app.component';
10 | import { HomeComponent } from './home.component';
11 | import { PageNotFoundComponent } from './page-not-found.component';
12 | import { PushComponent } from './push.component';
13 | import { SyncComponent } from './sync.component';
14 |
15 | @NgModule({
16 | declarations: [AppComponent, HomeComponent, PageNotFoundComponent, PushComponent, SyncComponent],
17 | imports: [
18 | BrowserModule,
19 | BrowserAnimationsModule,
20 | FormsModule,
21 | HttpClientModule,
22 | MdButtonModule,
23 | MdInputModule,
24 | MdToolbarModule,
25 | AppRoutingModule
26 | ],
27 | providers: [],
28 | bootstrap: [AppComponent]
29 | })
30 | export class AppModule {}
31 |
--------------------------------------------------------------------------------
/src/client/app/heroes/hero-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { HttpClient } from '@angular/common/http';
3 |
4 | @Component({
5 | selector: 'pwa-hero-list',
6 | template: `
7 |
8 |
hero-list works
9 |
10 |
11 | `
12 | })
13 | export class HeroListComponent implements OnInit {
14 | heroes: any;
15 |
16 | constructor(private http: HttpClient) {}
17 |
18 | ngOnInit() {
19 | this.http
20 | .get('/api/heroes')
21 | .subscribe(heroes => (this.heroes = heroes));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/client/app/heroes/heroes-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 | import { HeroListComponent } from './hero-list.component';
4 |
5 | const routes: Routes = [{ path: '', component: HeroListComponent }];
6 |
7 | @NgModule({
8 | imports: [RouterModule.forChild(routes)],
9 | exports: [RouterModule]
10 | })
11 | export class HeroesRoutingModule {}
12 |
--------------------------------------------------------------------------------
/src/client/app/heroes/heroes.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | import { HeroesRoutingModule } from './heroes-routing.module';
5 | import { HeroListComponent } from './hero-list.component';
6 |
7 | @NgModule({
8 | imports: [CommonModule, HeroesRoutingModule],
9 | declarations: [HeroListComponent]
10 | })
11 | export class HeroesModule {}
12 |
--------------------------------------------------------------------------------
/src/client/app/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'pwa-home',
5 | template: `
6 |
7 |
home works!
8 |
9 |
10 |
11 | `
12 | })
13 | export class HomeComponent implements OnInit {
14 | constructor() {}
15 |
16 | ngOnInit() {}
17 | }
18 |
--------------------------------------------------------------------------------
/src/client/app/page-not-found.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'pwa-page-not-found',
5 | template: `
6 | page-not-found
7 | `,
8 | styles: []
9 | })
10 | export class PageNotFoundComponent implements OnInit {
11 | constructor() {}
12 |
13 | ngOnInit() {}
14 | }
15 |
--------------------------------------------------------------------------------
/src/client/app/push.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | declare var Notification;
4 | // const applicationServerPublicKey =
5 | // 'BMZuj1Uek9SeT0myecw8TQxr4dB6Vl4X7c4abMzAA4KR72DsKnVcSpZr6svYgkwNSerKsz7vdZ1kfzwFc0TmH3o';
6 | const applicationServerPublicKey =
7 | 'BNKV7LJ5IFajn46I7FWroeSCMKtyOQPAGguMCn_-mVfyVjr_pvvQn0lW_KMoOAMqEAd4qhFHZhG6GEsDTPSJJ8I';
8 |
9 | @Component({
10 | selector: 'pwa-push',
11 | template: `
12 |
13 |
Receive Push Messages
14 |
15 |
Go here
16 | to get a key, then restart the app and send a push
17 |
18 |
{{subscriptionJson | json}}
19 |
20 | `
21 | })
22 | export class PushComponent implements OnInit {
23 | private isSubscribed = false;
24 | private registration = undefined;
25 |
26 | disablePushButton = false;
27 | pushButtonText = '';
28 | subscriptionJson = '';
29 |
30 | constructor() {}
31 |
32 | ngOnInit() {
33 | this.setupPush();
34 | }
35 |
36 | private setupPush() {
37 | if ('serviceWorker' in navigator && 'PushManager' in window) {
38 | navigator.serviceWorker.register('/sw.js').then(reg => {
39 | this.registration = reg;
40 | this.initializeUI();
41 | console.log('Service Worker and Push is supported');
42 | });
43 | } else {
44 | console.warn('Push messaging is not supported');
45 | this.disablePushButton = true;
46 | this.pushButtonText = 'Push Not Supported';
47 | }
48 | }
49 |
50 | subscribeClick() {
51 | this.disablePushButton = true;
52 | this.isSubscribed ? this.unsubscribeUser() : this.subscribeUser();
53 | }
54 |
55 | private initializeUI() {
56 | this.registration.pushManager.getSubscription().then(subscription => {
57 | this.isSubscribed = !(subscription === null);
58 | this.updateSubscriptionOnServer(subscription);
59 | console.log(`User ${this.isSubscribed ? 'IS' : 'is NOT'} subscribed.`);
60 | this.updateBtn();
61 | });
62 | }
63 |
64 | private unsubscribeUser() {
65 | this.registration.pushManager
66 | .getSubscription()
67 | .then(subscription => {
68 | if (subscription) {
69 | return subscription.unsubscribe();
70 | }
71 | })
72 | .catch(error => console.log('Error unsubscribing', error))
73 | .then(() => {
74 | this.updateSubscriptionOnServer(null);
75 | console.log('User is unsubscribed.');
76 | this.isSubscribed = false;
77 | this.updateBtn();
78 | });
79 | }
80 |
81 | private urlB64ToUint8Array(base64String) {
82 | const padding = '='.repeat((4 - base64String.length % 4) % 4);
83 | const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
84 | const rawData = window.atob(base64);
85 | const outputArray = new Uint8Array(rawData.length);
86 | for (let i = 0; i < rawData.length; ++i) {
87 | outputArray[i] = rawData.charCodeAt(i);
88 | }
89 | return outputArray;
90 | }
91 |
92 | subscribeUser() {
93 | const applicationServerKey = this.urlB64ToUint8Array(applicationServerPublicKey);
94 | this.registration.pushManager
95 | .subscribe({
96 | userVisibleOnly: true,
97 | applicationServerKey: applicationServerKey
98 | })
99 | .then(subscription => {
100 | console.log('User is subscribed.');
101 | this.updateSubscriptionOnServer(subscription);
102 | this.isSubscribed = true;
103 | this.updateBtn();
104 | })
105 | .catch(err => {
106 | console.log('Failed to subscribe the user: ', err);
107 | this.updateBtn();
108 | });
109 | }
110 |
111 | private updateSubscriptionOnServer(subscription) {
112 | const url = 'https://node-web-push-app.azurewebsites.net/subscribe';
113 |
114 | if (subscription) {
115 | subscription = JSON.stringify(subscription);
116 | this.subscriptionJson = subscription;
117 | const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: subscription };
118 | fetch(url, fetchOptions)
119 | .then(data => console.log('Push subscription request succeeded with JSON response', data))
120 | .catch(error => console.log('Push subscription request failed', error));
121 | } else {
122 | this.subscriptionJson = '';
123 | }
124 | }
125 |
126 | private updateBtn() {
127 | if (Notification.permission === 'denied') {
128 | this.pushButtonText = 'Push Messaging Blocked.';
129 | this.disablePushButton = true;
130 | this.updateSubscriptionOnServer(null);
131 | return;
132 | }
133 |
134 | this.pushButtonText = `${this.isSubscribed ? 'Disable' : 'Enable'} Push Messaging`;
135 | this.disablePushButton = false;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/client/app/sync.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | declare var idbKeyval;
4 | const key = 'pwa-messages';
5 |
6 | @Component({
7 | selector: 'pwa-sync',
8 | template: `
9 |
10 |
Send a message
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | `,
20 | styles: []
21 | })
22 | export class SyncComponent implements OnInit {
23 | message: { phone: string; body: string } = {
24 | phone: undefined,
25 | body: undefined
26 | };
27 |
28 | constructor() {}
29 |
30 | ngOnInit() {}
31 |
32 | isValidMessage() {
33 | return this.message && this.message.phone && this.message.body;
34 | }
35 |
36 | sendMessages() {
37 | if (this.isValidMessage()) {
38 | this.addToOutbox(this.message)
39 | .then(msg => navigator.serviceWorker.ready)
40 | .then(reg => this.registerSyncEvent(reg))
41 | .catch(() => this.sendMessage(this.message))
42 | .catch(err => console.log('unable to send messages to server', err));
43 | }
44 | }
45 |
46 | private addToOutbox(message) {
47 | return idbKeyval
48 | .get(key)
49 | .then(data => this.addMessageToArray(data, message))
50 | .then(messages => idbKeyval.set(key, JSON.stringify(messages)));
51 | }
52 |
53 | private addMessageToArray(data, message) {
54 | data = data || '[]';
55 | const messages = JSON.parse(data) || [];
56 | messages.push(message);
57 | return messages;
58 | }
59 |
60 | private removeLastMessageFromOutBox() {
61 | return this.getMessagesFromOutbox()
62 | .then(messages => messages.pop())
63 | .then(messages => idbKeyval.set(key, JSON.stringify(messages)))
64 | .then(() => console.log('message removed from outbox'))
65 | .catch(err => console.log('unable to remove message from outbox', err));
66 | }
67 |
68 | private getMessagesFromOutbox() {
69 | return idbKeyval.get(key).then(values => {
70 | values = values || '[]';
71 | const messages = JSON.parse(values) || [];
72 | return messages;
73 | });
74 | }
75 |
76 | private sendMessage(message) {
77 | const headers = {
78 | Accept: 'application/json',
79 | 'X-Requested-With': 'XMLHttpRequest',
80 | 'Content-Type': 'application/json'
81 | };
82 | const msg = {
83 | method: 'POST',
84 | body: JSON.stringify(message),
85 | headers: headers
86 | };
87 | return fetch('/messages', msg)
88 | .then(response => {
89 | console.log('message sent!', message);
90 | return response.json();
91 | })
92 | .then(() => this.removeLastMessageFromOutBox())
93 | .catch(err =>
94 | console.log('server unable to handle the message', message, err)
95 | );
96 | }
97 |
98 | private registerSyncEvent(reg) {
99 | return reg.sync
100 | .register('my-pwa-messages')
101 | .then(() => console.log('Sync - registered for my-pwa-messages'))
102 | .catch(() => console.log('Sync - registration failed'));
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/client/app/villains/villain-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { HttpClient } from '@angular/common/http';
3 |
4 | @Component({
5 | selector: 'pwa-villain-list',
6 | template: `
7 |
8 |
villain-list works
9 |
10 |
11 | `
12 | })
13 | export class VillainListComponent implements OnInit {
14 | villains: any;
15 |
16 | constructor(private http: HttpClient) {}
17 |
18 | ngOnInit() {
19 | this.http.get('/api/villains').subscribe(villains => (this.villains = villains));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/client/app/villains/villains-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 | import { VillainListComponent } from './villain-list.component';
4 |
5 | const routes: Routes = [{ path: '', component: VillainListComponent }];
6 |
7 | @NgModule({
8 | imports: [RouterModule.forChild(routes)],
9 | exports: [RouterModule]
10 | })
11 | export class VillainsRoutingModule {}
12 |
--------------------------------------------------------------------------------
/src/client/app/villains/villains.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | import { VillainsRoutingModule } from './villains-routing.module';
5 | import { VillainListComponent } from './villain-list.component';
6 |
7 | @NgModule({
8 | imports: [CommonModule, VillainsRoutingModule],
9 | declarations: [VillainListComponent]
10 | })
11 | export class VillainsModule {}
12 |
--------------------------------------------------------------------------------
/src/client/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/pwa-angular/f6b222c51aaa8af645abba1d9846d8feff7f5862/src/client/assets/.gitkeep
--------------------------------------------------------------------------------
/src/client/assets/idb-keyval-min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";function e(){return t||(t=new Promise(function(e,n){var t=indexedDB.open("keyval-store",1);t.onerror=function(){n(t.error)},t.onupgradeneeded=function(){t.result.createObjectStore("keyval")},t.onsuccess=function(){e(t.result)}})),t}function n(n,t){return e().then(function(e){return new Promise(function(r,o){var u=e.transaction("keyval",n);u.oncomplete=function(){r()},u.onerror=function(){o(u.error)},t(u.objectStore("keyval"))})})}var t,r={get:function(e){var t;return n("readonly",function(n){t=n.get(e)}).then(function(){return t.result})},set:function(e,t){return n("readwrite",function(n){n.put(t,e)})},"delete":function(e){return n("readwrite",function(n){n["delete"](e)})},clear:function(){return n("readwrite",function(e){e.clear()})},keys:function(){var e=[];return n("readonly",function(n){(n.openKeyCursor||n.openCursor).call(n).onsuccess=function(){this.result&&(e.push(this.result.key),this.result["continue"]())}}).then(function(){return e})}};"undefined"!=typeof module&&module.exports?module.exports=r:"function"==typeof define&&define.amd?define("idbKeyval",[],function(){return r}):self.idbKeyval=r}();
2 |
--------------------------------------------------------------------------------
/src/client/assets/launcher-icon-2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/pwa-angular/f6b222c51aaa8af645abba1d9846d8feff7f5862/src/client/assets/launcher-icon-2x.jpg
--------------------------------------------------------------------------------
/src/client/assets/launcher-icon-3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/pwa-angular/f6b222c51aaa8af645abba1d9846d8feff7f5862/src/client/assets/launcher-icon-3x.jpg
--------------------------------------------------------------------------------
/src/client/assets/launcher-icon-4x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/pwa-angular/f6b222c51aaa8af645abba1d9846d8feff7f5862/src/client/assets/launcher-icon-4x.jpg
--------------------------------------------------------------------------------
/src/client/assets/ng.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/pwa-angular/f6b222c51aaa8af645abba1d9846d8feff7f5862/src/client/assets/ng.png
--------------------------------------------------------------------------------
/src/client/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/client/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/client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnpapa/pwa-angular/f6b222c51aaa8af645abba1d9846d8feff7f5862/src/client/favicon.ico
--------------------------------------------------------------------------------
/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PwaAngular
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Loading...
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/client/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .then(() => registerServiceWorker());
14 |
15 | let registration = undefined;
16 |
17 | function registerServiceWorker() {
18 | if ('serviceWorker' in navigator) {
19 | navigator.serviceWorker
20 | .register('/sw.js')
21 | .then(reg => {
22 | registration = reg;
23 | swLog('Registration successful', registration);
24 | registration.onupdatefound = () => checkServiceWorkerStateChange();
25 | })
26 | .catch(e =>
27 | console.error('Error during service worker registration:', e)
28 | );
29 | } else {
30 | console.warn('Service Worker is not supported');
31 | }
32 | }
33 |
34 | function checkServiceWorkerStateChange() {
35 | const installingWorker = registration.installing;
36 |
37 | installingWorker.onstatechange = () => {
38 | switch (installingWorker.state) {
39 | case 'installed':
40 | if (navigator.serviceWorker.controller) {
41 | swLog('New or updated content is available', installingWorker);
42 | } else {
43 | swLog('Content is now available offline', installingWorker);
44 | }
45 | break;
46 | case 'redundant':
47 | console.error(
48 | 'The installing service worker became redundant',
49 | installingWorker
50 | );
51 | break;
52 | default:
53 | swLog(installingWorker.state);
54 | break;
55 | }
56 | };
57 | }
58 |
59 | function swLog(eventName, event?) {
60 | console.log('Service Worker - ' + eventName);
61 | if (event) {
62 | console.log(event);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/client/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/web-manifest",
3 | "short_name": "Angular PWA",
4 | "name": "Angular PWA by John Papa",
5 | "description": "Progressive Web Application demonstration with Angular.",
6 | "icons": [
7 | {
8 | "src": "assets/launcher-icon-2x.jpg",
9 | "sizes": "96x96",
10 | "type": "image/jpg"
11 | },
12 | {
13 | "src": "assets/launcher-icon-3x.jpg",
14 | "sizes": "144x144",
15 | "type": "image/jpg"
16 | },
17 | {
18 | "src": "assets/launcher-icon-4x.jpg",
19 | "sizes": "192x192",
20 | "type": "image/jpg"
21 | },
22 | {
23 | "src": "assets/launcher-icon-4x.jpg",
24 | "sizes": "512x512",
25 | "type": "image/jpg"
26 | }
27 | ],
28 | "background_color": "#EEE",
29 | "theme_color": "#1565c0",
30 | "start_url": "./?utm_source=web_app_manifest",
31 | "display": "standalone",
32 | "orientation": "any"
33 | }
34 |
--------------------------------------------------------------------------------
/src/client/offline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 | PWA Angular
22 |
23 | We're offline. Please go outside and enjoy the day!
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/client/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/set';
35 |
36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
37 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
38 |
39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */
40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
41 |
42 |
43 | /** Evergreen browsers require these. **/
44 | import 'core-js/es6/reflect';
45 | import 'core-js/es7/reflect';
46 |
47 |
48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/
49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
50 |
51 |
52 |
53 | /***************************************************************************************************
54 | * Zone JS is required by Angular itself.
55 | */
56 | import 'zone.js/dist/zone'; // Included with Angular CLI.
57 |
58 |
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
64 | /**
65 | * Date, currency, decimal and percent pipes.
66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
67 | */
68 | // import 'intl'; // Run `npm install --save intl`.
69 | /**
70 | * Need to import at least one locale-data with intl.
71 | */
72 | // import 'intl/locale-data/jsonp/en';
73 |
--------------------------------------------------------------------------------
/src/client/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @import 'theme';
2 |
3 | @mixin primary-color {
4 | $primary-color: map-get($primary, 800);
5 | color: $primary-color;
6 | }
7 | @mixin primary-background-contrast-color {
8 | $primary-color: map-get($primary, 400);
9 | $font-color: map-get(map-get($primary, contrast), 400);
10 | background-color: $primary-color;
11 | color: $font-color !important;
12 | }
13 | @mixin accent-color {
14 | $accent-font-color: map-get($accent, 800);
15 | color: $accent-font-color;
16 | }
17 | @mixin selected-style {
18 | $background-color: map-get($accent, 800);
19 | $font-color: map-get(map-get($accent, contrast), 800);
20 | background: $background-color !important;
21 | color: $font-color !important;
22 | }
23 | @mixin hover-style {
24 | $background-color: map-get($accent, 400);
25 | $font-color: map-get(map-get($accent, contrast), 900);
26 | background: $background-color !important;
27 | color: $font-color !important;
28 | }
29 | @mixin selected-hover-style {
30 | $background-color: map-get($accent, 600);
31 | $font-color: map-get(map-get($accent, contrast), 600);
32 | background: $background-color !important;
33 | color: $font-color !important;
34 | }
35 |
36 |
37 |
38 | // @mixin primary-color-100 {
39 | // $primary-color: map-get($primary, 100);
40 | // background-color: $primary-color;
41 | // }
42 | // @mixin primary-color-200 {
43 | // $primary-color: map-get($primary, 200);
44 | // background-color: $primary-color;
45 | // }
46 | // @mixin primary-color-300 {
47 | // $primary-color: map-get($primary, 300);
48 | // background-color: $primary-color;
49 | // }
50 | // @mixin primary-color-400 {
51 | // $primary-color: map-get($primary, 400);
52 | // background-color: $primary-color;
53 | // }
54 | // @mixin primary-color-500 {
55 | // $primary-color: map-get($primary, 500);
56 | // background-color: $primary-color;
57 | // }
58 | // @mixin primary-color-600 {
59 | // $primary-color: map-get($primary, 600);
60 | // background-color: $primary-color;
61 | // }
62 | // @mixin primary-color-700 {
63 | // $primary-color: map-get($primary, 700);
64 | // background-color: $primary-color;
65 | // }
66 | // @mixin primary-color-800 {
67 | // $primary-color: map-get($primary, 800);
68 | // background-color: $primary-color;
69 | // }
70 |
71 | // @mixin accent-color-100 {
72 | // $accent-color: map-get($accent, 100);
73 | // background-color: $accent-color;
74 | // }
75 | // @mixin accent-color-200 {
76 | // $accent-color: map-get($accent, 200);
77 | // background-color: $accent-color;
78 | // }
79 | // @mixin accent-color-300 {
80 | // $accent-color: map-get($accent, 300);
81 | // background-color: $accent-color;
82 | // }
83 | // @mixin accent-color-400 {
84 | // $accent-color: map-get($accent, 400);
85 | // background-color: $accent-color;
86 | // }
87 | // @mixin accent-color-500 {
88 | // $accent-color: map-get($accent, 500);
89 | // background-color: $accent-color;
90 | // }
91 | // @mixin accent-color-600 {
92 | // $accent-color: map-get($accent, 600);
93 | // background-color: $accent-color;
94 | // }
95 | // @mixin accent-color-700 {
96 | // $accent-color: map-get($accent, 700);
97 | // background-color: $accent-color;
98 | // }
99 | // @mixin accent-color-800 {
100 | // $accent-color: map-get($accent, 800);
101 | // background-color: $accent-color;
102 | // }
103 |
--------------------------------------------------------------------------------
/src/client/styles/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @import 'mixin';
3 |
4 | html, body {
5 | font-family: 'Roboto', sans-serif;
6 | margin: 0px;
7 | }
8 | .content {
9 | margin: 1em;
10 | }
11 | .ng-title-icon > i {
12 | background-image: url("../assets/ng.png");
13 | }
14 | .ng-title-icon > i, azfunc-title-icon > i {
15 | background-repeat: no-repeat;
16 | background-position: center center;
17 | padding: 1.2em;
18 | }
19 | .title {
20 | margin-right: 1em;
21 | }
22 | .router-link-active {
23 | @include primary-background-contrast-color;
24 | }
25 | pre {
26 | background-color: map-get($accent, 100);
27 | padding: 1em;
28 | font-size: 12px;
29 | }
30 |
31 | div.content {
32 | margin:1em;
33 | }
34 |
35 | .progress {
36 | display: flex;
37 | align-content: center;
38 | align-items: center;
39 | margin: 2em 0em;
40 | }
41 |
42 | .snack {
43 | @include selected-style;
44 | }
45 |
46 | .pull-right {
47 | position:fixed !important;
48 | right: 8px;
49 | }
50 |
51 | @media (max-width: 414px) {
52 | .pull-right {
53 | display: none !important;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/client/styles/theme.scss:
--------------------------------------------------------------------------------
1 | // https://material.angular.io/guide/theming
2 | @import '~@angular/material/theming';
3 | @include mat-core();
4 |
5 | $primary: mat-palette($mat-blue, 800);
6 | $accent: mat-palette($mat-pink, A200, A100, A400);
7 | $warn: mat-palette($mat-deep-orange);
8 | $light-theme: mat-light-theme($primary, $accent, $warn);
9 | @include angular-material-theme($light-theme);
10 |
--------------------------------------------------------------------------------
/src/client/sw.js:
--------------------------------------------------------------------------------
1 | // https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
2 |
3 | // ------------------------------
4 | // Pre Cache and Update
5 | // ------------------------------
6 | importScripts('./workbox-sw.prod.v2.0.0.js');
7 |
8 | /**
9 | * Create an instance of WorkboxSW.
10 | * Setting clientsClaims to true tells our service worker to take control as
11 | * soon as it's activated.
12 | */
13 | const workboxSW = new WorkboxSW({ clientsClaim: true });
14 |
15 | /**
16 | * precache() is passed a manifest of URLs and versions, and does the following
17 | * each time the service worker starts up:
18 | * - Adds all new URLs to a cache.
19 | * - Refreshes the previously cached response if the URL isn't new, but the
20 | * revision changes. This will also trigger a Broadcast Channel API message
21 | * sent to the channel 'precache-updates'.
22 | * - Removes entries for URLs that used to be in the list, but aren't anymore.
23 | * - Sets up a fetch handler to respond to any requests for URLs in this
24 | * list using a cache-first strategy.
25 | *
26 | * DO NOT CREATE OR UPDATE THIS LIST BY HAND!
27 | * Instead, add one of our tools (workbox-cli, workbox-webpack-plugin, or
28 | * workbox-build) to your existing build process, and have that regenerate the
29 | * manifest at the end of every build.
30 | */
31 |
32 | // An array of file details include a `url` and `revision` parameter.
33 | workboxSW.precache([]);
34 |
35 | /**
36 | * Requests for URLs that aren't precached can be handled by runtime caching.
37 | * Workbox has a flexible routing system, giving you control over which caching
38 | * strategies to use for which kind of requests.
39 | *
40 | * registerRoute() takes a RegExp or a string as its first parameter.
41 | * - RegExps can match any part of the request URL.
42 | * - Strings are Express-style routes, parsed by
43 | * https://github.com/nightwolfz/path-to-regexp
44 | *
45 | * registerRoute() takes a caching strategy as its second parameter.
46 | * The built-in strategies are:
47 | * - cacheFirst
48 | * - cacheOnly
49 | * - networkFirst
50 | * - networkOnly
51 | * - staleWhileRevalidate
52 | * Advice about which strategies to use for various assets can be found at
53 | * https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/
54 | *
55 | * Each strategy can be configured with additional options, controlling the
56 | * name of the cache that's used, cache expiration policies, which response
57 | * codes are considered valid (useful when you want to cache opaque responses)
58 | * and whether updates to previously cached responses should trigger a message
59 | * using the BroadcastChannel API.
60 | *
61 | * The following routes show this flexibility put to use.
62 | */
63 |
64 | /**
65 | * Set up a route that will match any URL requested that ends in .json.
66 | * Handle those requests using a network-first strategy.
67 | */
68 | // workboxSW.router.registerRoute(
69 | // /\.json$/,
70 | // workboxSW.strategies.networkFirst()
71 | // );
72 |
73 | /**
74 | * Set up a route that will match any URL requested that has /api/ in it.
75 | * Handle those requests using a network-first strategy, but with a timeout.
76 | * If there's no network response before the timeout, then return the previous
77 | * response from the cache instead.
78 | */
79 |
80 | workboxSW.router.registerRoute(
81 | /\/api\/(.*)/,
82 | // workboxSW.strategies.networkFirst({ networkTimeoutSeconds: 1 })
83 | workboxSW.strategies.cacheFirst({ cacheName: 'hero-api' })
84 | );
85 |
86 | // don't need this since we have fallback
87 | // const networkFirstStrategy = workboxSW.strategies.networkFirst();
88 | // workboxSW.router.registerRoute('/home/', networkFirstStrategy);
89 | // workboxSW.router.registerRoute('/heroes/', networkFirstStrategy);
90 | // workboxSW.router.registerRoute('/villains/', networkFirstStrategy);
91 |
92 | /**
93 | * This URL will be used as a fallback if a navigation request can't be fulfilled.
94 | * Normally this URL would be precached so it's always available.
95 | * This is particularly useful for single page apps where requests should go to a single URL.
96 | */
97 | workboxSW.router.registerNavigationRoute('/index.html');
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | // -------------------------------------------------------
111 | // background sync
112 | // -------------------------------------------------------
113 | self.importScripts('assets/idb-keyval-min.js');
114 |
115 | self.addEventListener('sync', event => {
116 | swLog('I heard a sync event!', event);
117 | if (event.tag === 'my-pwa-messages') {
118 | event.waitUntil(getMessagesFromOutbox()
119 | .then(messages => Promise.all(mapAndSendMessages(messages)))
120 | .catch(err => swLog('unable to send messages to server', err))
121 | .then(response => removeMessagesFromOutBox(response))
122 | );
123 | }
124 | });
125 |
126 | function getMessagesFromOutbox() {
127 | const key = 'pwa-messages';
128 | return idbKeyval.get(key).then(values => {
129 | values = values || '[]';
130 | const messages = JSON.parse(values) || [];
131 | return messages;
132 | });
133 | }
134 |
135 | function mapAndSendMessages(messages) {
136 | return messages.map(
137 | message => sendMessage(message)
138 | .then(response => response.json())
139 | .catch(err => swLog('server unable to handle the message', message, err))
140 | );
141 | }
142 |
143 | function sendMessage(message) {
144 | const headers = {
145 | 'Accept': 'application/json',
146 | 'X-Requested-With': 'XMLHttpRequest',
147 | 'Content-Type': 'application/json'
148 | };
149 | const msg = {
150 | method: 'POST',
151 | body: JSON.stringify(message),
152 | headers: headers
153 | };
154 | return fetch('/messages', msg).then((response) => {
155 | swLog('message sent!', message);
156 | return response;
157 | });
158 | }
159 |
160 | function removeMessagesFromOutBox(response) {
161 | // If the first worked,let's assume for now they all did
162 | if (response && response.length && response[0] && response[0].result === 'success') {
163 | return idbKeyval.clear()
164 | .then(() => swLog('messages removed from outbox'))
165 | .catch(err => swLog('unable to remove messages from outbox', err));
166 | }
167 | return Promise.resolve(true);
168 | }
169 |
170 |
171 | // -------------------------------------------------------
172 | // push
173 | // -------------------------------------------------------
174 | // https://github.com/web-push-libs/web-push
175 | self.addEventListener('push', event => {
176 | const body = event.data.text() || 'A little push';
177 | swLog(`Push received and had this data: "${event.data.text()}"`);
178 |
179 | const title = 'Push Demo';
180 | const options = {
181 | body: body,
182 | icon: 'assets/ng.png',
183 | badge: 'assets/ng.png'
184 | };
185 |
186 | event.waitUntil(self.registration.showNotification(title, options));
187 | });
188 |
189 | self.addEventListener('notificationclick', event => {
190 | swLog('Notification click Received.');
191 |
192 | event.notification.close();
193 |
194 | // We are calling event.waitUntil() again
195 | // to ensure the browser doesn't terminate
196 | // our service worker before our new window has been displayed.
197 | event.waitUntil(clients.openWindow('https://johnpapa.net'));
198 | });
199 |
200 | // const applicationServerPublicKey = 'BMZuj1Uek9SeT0myecw8TQxr4dB6Vl4X7c4abMzAA4KR72DsKnVcSpZr6svYgkwNSerKsz7vdZ1kfzwFc0TmH3o';
201 | const applicationServerPublicKey =
202 | 'BNKV7LJ5IFajn46I7FWroeSCMKtyOQPAGguMCn_-mVfyVjr_pvvQn0lW_KMoOAMqEAd4qhFHZhG6GEsDTPSJJ8I';
203 |
204 | self.addEventListener('pushsubscriptionchange', event => {
205 | swLog(`'pushsubscriptionchange' event fired.`);
206 | const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
207 | event.waitUntil(
208 | self.registration.pushManager.subscribe({
209 | userVisibleOnly: true,
210 | applicationServerKey: applicationServerKey
211 | })
212 | .then(newSubscription => {
213 | // TODO: Send to application server
214 | swLog('New subscription: ', newSubscription);
215 | })
216 | );
217 | });
218 |
219 |
220 |
221 | // -------------------------------------------------------
222 | // logging
223 | // -------------------------------------------------------
224 | function swLog(eventName, event) {
225 | console.log('[Service Worker] ' + eventName);
226 | if (event) {
227 | console.log(event);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/client/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare var __karma__: any;
17 | declare var require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting()
26 | );
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().map(context);
31 | // Finally, start Karma to run the tests.
32 | __karma__.start();
33 |
--------------------------------------------------------------------------------
/src/client/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/app",
5 | "module": "es2015",
6 | "baseUrl": "",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/client/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/spec",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "baseUrl": "",
8 | "types": [
9 | "jasmine",
10 | "node"
11 | ]
12 | },
13 | "files": [
14 | "test.ts"
15 | ],
16 | "include": [
17 | "**/*.spec.ts",
18 | "**/*.d.ts"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/client/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/server/api/heroes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "Luke Skywalker",
5 | "allegiance": "jedi"
6 | },
7 | {
8 | "id": 2,
9 | "name": "C-3PO",
10 | "allegiance": "rebel"
11 | },
12 | {
13 | "id": 3,
14 | "name": "R2-D2",
15 | "allegiance": "rebel"
16 | },
17 | {
18 | "id": 5,
19 | "name": "Leia Organa",
20 | "allegiance": "rebel"
21 | },
22 | {
23 | "id": 10,
24 | "name": "Obi-Wan Kenobi",
25 | "allegiance": "jedi"
26 | },
27 | {
28 | "id": 13,
29 | "name": "Chewbacca",
30 | "allegiance": "rebel"
31 | },
32 | {
33 | "id": 14,
34 | "name": "Han Solo",
35 | "allegiance": "rebel"
36 | },
37 | {
38 | "id": 17,
39 | "name": "Wedge Antilles",
40 | "allegiance": "rebel"
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/src/server/api/villains.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 4,
4 | "name": "Darth Vader",
5 | "allegiance": "sith"
6 | },
7 | {
8 | "id": 11,
9 | "name": "Anakin Skywalker",
10 | "allegiance": "jedi"
11 | },
12 | {
13 | "id": 15,
14 | "name": "Greedo",
15 | "allegiance": "bounty hunter"
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const fs = require('fs');
4 | const bodyParser = require('body-parser');
5 | const routes = require('./routes');
6 |
7 | const root = './public';
8 | const public = process.env.PUBLIC || `${root}`;
9 | const app = express();
10 |
11 | app.use(bodyParser.urlencoded({ extended: true }));
12 | app.use(bodyParser.json());
13 |
14 | app.use('/api', routes);
15 |
16 | const twilioSettings = {
17 | accountSid: process.env.TWILIO_ACCOUNT_SID,
18 | authToken: process.env.TWILIO_AUTH_TOKEN,
19 | phone: process.env.TWILIO_PHONE
20 | };
21 |
22 | app.get('/ping', (req, res, next) => {
23 | console.log(req.body);
24 | res.send('pong');
25 | });
26 |
27 | app.get('/api/heroes', (req, res, next) => {
28 | console.log(req.body);
29 | res.sendFile('../api/heroes.json', { root: root });
30 | });
31 |
32 | app.get('../api/villains', (req, res, next) => {
33 | console.log(req.body);
34 | res.sendFile('api/villains.json', { root: root });
35 | });
36 |
37 | app.post('/messages', (req, res, next) => {
38 | try {
39 | const twilio = require('twilio');
40 | const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
41 |
42 | var msg = {
43 | from: twilioSettings.phone,
44 | to: req.body.phone,
45 | body: req.body.body
46 | };
47 | console.log('sending', msg);
48 | client.messages
49 | .create(msg)
50 | .then(data => {
51 | if (req.xhr) {
52 | res.setHeader('Content-Type', 'application/json');
53 | res.send(JSON.stringify({ result: 'success' }));
54 | } else {
55 | res.redirect('/messages/' + msg.phone + '#' + data.sid);
56 | }
57 | })
58 | .catch(err => {
59 | if (req.xhr) {
60 | res.setHeader('Content-Type', 'application/json');
61 | res.status(err.status).send(JSON.stringify(err));
62 | } else {
63 | res.redirect(req.header('Referer') || '/');
64 | }
65 | });
66 | } catch (error) {
67 | const msg = "twilio failed, but that's ok. We'll move along";
68 | console.log(msg);
69 | res.status(500).send(msg);
70 | }
71 | });
72 |
73 | app.use(express.static(public));
74 | console.log(`serving ${public}`);
75 |
76 | // app.use(express.static('./'));
77 | // Any deep link calls should return index.html
78 | // app.use('/*', express.static('./index.html'));
79 |
80 | // app.use((req, res, next) => {
81 | // // if the request is not html then move along
82 | // var accept = req.accepts('html', 'json', 'xml');
83 | // if (accept !== 'html') {
84 | // return next();
85 | // }
86 |
87 | // // // if the request has a '.' assume that it's for a file, move along
88 | // // var ext = path.extname(req.path);
89 | // // if (ext !== '') {
90 | // // return next();
91 | // // }
92 |
93 | // fs.createReadStream(staticRoot + 'index.html').pipe(res);
94 | // });
95 |
96 | app.get('*', (req, res) => {
97 | res.sendFile('index.html', { root: root });
98 | });
99 |
100 | const port = process.env.PORT || '4201';
101 | app.listen(port, () => console.log(`API running on localhost:${port}`));
102 |
--------------------------------------------------------------------------------
/src/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pwa-angular",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "start": "node index.js"
6 | },
7 | "engines": {
8 | "node": "6.9.1"
9 | },
10 | "dependencies": {
11 | "express": "^4.15.3",
12 | "body-parser": "^1.17.2",
13 | "twilio": "^3.3.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/server/routes.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const heroes = require('./api/heroes.json');
5 | const villains = require('./api/villains.json');
6 |
7 | router.get('/heroes', (req, res) => {
8 | res.status(200).json(heroes);
9 | });
10 |
11 | router.get('/villains', (req, res) => {
12 | res.status(200).json(villains);
13 | });
14 |
15 | module.exports = router;
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "baseUrl": "src",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es5",
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2016",
17 | "dom"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "callable-types": true,
7 | "class-name": true,
8 | "comment-format": [
9 | true,
10 | "check-space"
11 | ],
12 | "curly": true,
13 | "eofline": true,
14 | "forin": true,
15 | "import-blacklist": [
16 | true,
17 | "rxjs"
18 | ],
19 | "import-spacing": true,
20 | "indent": [
21 | true,
22 | "spaces"
23 | ],
24 | "interface-over-type-literal": true,
25 | "label-position": true,
26 | "max-line-length": [
27 | true,
28 | 140
29 | ],
30 | "member-access": false,
31 | "member-ordering": [
32 | true,
33 | "static-before-instance",
34 | "variables-before-functions"
35 | ],
36 | "no-arg": true,
37 | "no-bitwise": true,
38 | "no-console": [
39 | true,
40 | "debug",
41 | "info",
42 | "time",
43 | "timeEnd",
44 | "trace"
45 | ],
46 | "no-construct": true,
47 | "no-debugger": true,
48 | "no-empty": false,
49 | "no-empty-interface": true,
50 | "no-eval": true,
51 | "no-inferrable-types": [
52 | true,
53 | "ignore-params"
54 | ],
55 | "no-shadowed-variable": true,
56 | "no-string-literal": false,
57 | "no-string-throw": true,
58 | "no-switch-case-fall-through": true,
59 | "no-trailing-whitespace": true,
60 | "no-unused-expression": true,
61 | "no-use-before-declare": true,
62 | "no-var-keyword": true,
63 | "object-literal-sort-keys": false,
64 | "one-line": [
65 | true,
66 | "check-open-brace",
67 | "check-catch",
68 | "check-else",
69 | "check-whitespace"
70 | ],
71 | "prefer-const": true,
72 | "quotemark": [
73 | true,
74 | "single"
75 | ],
76 | "radix": true,
77 | "semicolon": [
78 | "always"
79 | ],
80 | "triple-equals": [
81 | true,
82 | "allow-null-check"
83 | ],
84 | "typedef-whitespace": [
85 | true,
86 | {
87 | "call-signature": "nospace",
88 | "index-signature": "nospace",
89 | "parameter": "nospace",
90 | "property-declaration": "nospace",
91 | "variable-declaration": "nospace"
92 | }
93 | ],
94 | "typeof-compare": true,
95 | "unified-signatures": true,
96 | "variable-name": false,
97 | "whitespace": [
98 | true,
99 | "check-branch",
100 | "check-decl",
101 | "check-operator",
102 | "check-separator",
103 | "check-type"
104 | ],
105 | "directive-selector": [
106 | true,
107 | "attribute",
108 | "pwa",
109 | "camelCase"
110 | ],
111 | "component-selector": [
112 | true,
113 | "element",
114 | "pwa",
115 | "kebab-case"
116 | ],
117 | "use-input-property-decorator": true,
118 | "use-output-property-decorator": true,
119 | "use-host-property-decorator": true,
120 | "no-input-rename": true,
121 | "no-output-rename": true,
122 | "use-life-cycle-interface": true,
123 | "use-pipe-transform-interface": true,
124 | "component-class-suffix": true,
125 | "directive-class-suffix": true,
126 | "no-access-missing-member": true,
127 | "templates-use-public": true,
128 | "invoke-injectable": true
129 | }
130 | }
131 |
--------------------------------------------------------------------------------