├── .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 |
8 | 9 | 10 | 11 | home 13 | heroes 15 | villains 17 | John Papa 18 |
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 | --------------------------------------------------------------------------------