├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── browserslist ├── ionic.config.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.scss │ ├── interfaces │ │ └── user-options.ts │ ├── pages │ │ ├── about-popover │ │ │ └── about-popover.ts │ │ ├── about │ │ │ ├── about-routing.module.ts │ │ │ ├── about.html │ │ │ ├── about.module.ts │ │ │ ├── about.scss │ │ │ └── about.ts │ │ ├── account │ │ │ ├── account-routing.module.ts │ │ │ ├── account.html │ │ │ ├── account.module.ts │ │ │ ├── account.scss │ │ │ └── account.ts │ │ ├── login │ │ │ ├── login-routing.module.ts │ │ │ ├── login.html │ │ │ ├── login.module.ts │ │ │ ├── login.scss │ │ │ └── login.ts │ │ ├── map │ │ │ ├── map-dark-style.js │ │ │ ├── map-routing.module.ts │ │ │ ├── map.html │ │ │ ├── map.module.ts │ │ │ ├── map.scss │ │ │ └── map.ts │ │ ├── schedule-filter │ │ │ ├── schedule-filter.html │ │ │ ├── schedule-filter.scss │ │ │ └── schedule-filter.ts │ │ ├── schedule │ │ │ ├── schedule-routing.module.ts │ │ │ ├── schedule.html │ │ │ ├── schedule.module.ts │ │ │ ├── schedule.scss │ │ │ └── schedule.ts │ │ ├── session-detail │ │ │ ├── session-detail-routing.module.ts │ │ │ ├── session-detail.html │ │ │ ├── session-detail.module.ts │ │ │ ├── session-detail.scss │ │ │ └── session-detail.ts │ │ ├── signup │ │ │ ├── signup-routing.module.ts │ │ │ ├── signup.html │ │ │ ├── signup.module.ts │ │ │ ├── signup.scss │ │ │ └── signup.ts │ │ ├── speaker-detail │ │ │ ├── speaker-detail-routing.module.ts │ │ │ ├── speaker-detail.html │ │ │ ├── speaker-detail.module.ts │ │ │ ├── speaker-detail.scss │ │ │ └── speaker-detail.ts │ │ ├── speaker-list │ │ │ ├── speaker-list-routing.module.ts │ │ │ ├── speaker-list.html │ │ │ ├── speaker-list.module.ts │ │ │ ├── speaker-list.scss │ │ │ └── speaker-list.ts │ │ ├── support │ │ │ ├── support-routing.module.ts │ │ │ ├── support.html │ │ │ ├── support.module.ts │ │ │ ├── support.scss │ │ │ └── support.ts │ │ ├── tabs-page │ │ │ ├── tabs-page-routing.module.ts │ │ │ ├── tabs-page.html │ │ │ ├── tabs-page.module.ts │ │ │ ├── tabs-page.scss │ │ │ └── tabs-page.ts │ │ └── tutorial │ │ │ ├── tutorial-routing.module.ts │ │ │ ├── tutorial.html │ │ │ ├── tutorial.module.ts │ │ │ ├── tutorial.scss │ │ │ └── tutorial.ts │ └── providers │ │ ├── check-tutorial.service.ts │ │ ├── conference-data.ts │ │ └── user-data.ts ├── assets │ ├── data │ │ └── data.json │ ├── icons │ │ └── .gitkeep │ └── img │ │ ├── appicon.png │ │ ├── appicon.svg │ │ ├── ica-slidebox-img-1.png │ │ ├── ica-slidebox-img-2.png │ │ ├── ica-slidebox-img-3.png │ │ ├── ica-slidebox-img-4.png │ │ ├── ionic-logo-white.svg │ │ └── speakers │ │ ├── bear.jpg │ │ ├── cheetah.jpg │ │ ├── duck.jpg │ │ ├── eagle.jpg │ │ ├── elephant.jpg │ │ ├── giraffe.jpg │ │ ├── iguana.jpg │ │ ├── kitten.jpg │ │ ├── lion.jpg │ │ ├── mouse.jpg │ │ ├── puppy.jpg │ │ ├── rabbit.jpg │ │ └── turtle.jpg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss └── zone-flags.ts ├── superstatic.json ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | www/ 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | *.log 8 | *.tmp 9 | *.tmp.* 10 | log.txt 11 | *.sublime-project 12 | *.sublime-workspace 13 | .vscode/ 14 | npm-debug.log* 15 | .firebase/ 16 | .idea/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | dist/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | hooks/ 27 | platforms/ 28 | plugins/ 29 | plugins/android.json 30 | plugins/ios.json 31 | $RECYCLE.BIN/ 32 | package-lock.json 33 | 34 | .DS_Store 35 | Thumbs.db 36 | UserInterfaceState.xcuserstate 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Drifty Co. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a production ready PWA with Angular and Firebase 2 | 3 | ![PWA LOGO](https://user-images.githubusercontent.com/3104648/28351989-7f68389e-6c4b-11e7-9bf2-e9fcd4977e7a.png) 4 | 5 | Welcome to the workshop of building a production ready Progressive Web App based on Angular, Ionic and Firebase. 6 | 7 | This workshop consists of multiple steps for producing a PWA by transforming a regular web app - [Ionic Conference App](https://github.com/ionic-team/ionic-conference-app/) into a PWA, optimizing it for various platforms and powering it up with Firebase services. 8 | 9 | ## Ionic Conference App 10 | 11 | The end application is purely a kitchen-sink demo of a PWA based on Ionic Framework and Angular. We will be using some of Ionic's UI components and services with Angular for creating a uniform and native-like user experiences in multiple platforms. 12 | 13 | **We will not use Ionic for building a hybrid mobile app, but instead we will use it for building a PWA!** 14 | 15 | | Material Design | iOS | 16 | | -----------------| -----| 17 | | ![Android Schedule](https://github.com/ionic-team/ionic-conference-app/raw/master/resources/screenshots/android-schedule.png) | ![iOS Schedule](https://github.com/ionic-team/ionic-conference-app/raw/master/resources/screenshots/ios-schedule.png) | 18 | 19 | ## How this workshop works? 20 | 21 | Since we depend on Angular CLI and some other tools which are not available for online code editor environments, we're going to develop and build the app on our local computers. 22 | 23 | **Every next step/branch includes the solution of the previous step.** 24 | 25 | > If you're stuck at any of the steps, you can switch to the next step/branch and continue from there. Note that you need to discard your local changes on git when you checkout a solution. 26 | 27 | ## Requirements for local development environment 28 | 29 | - Google Chrome - [Download](https://www.google.com/chrome/) 30 | - node.js > 8.0.0 & npm > 5.2.0 - [Download](https://nodejs.org/en/) 31 | - Open a Firebase Account (FREE) - [Link](https://firebase.google.com/) 32 | - GIT - [Download](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 33 | - Clone this repo - Run `git clone https://github.com/onderceylan/pwa-workshop-angular-firebase` 34 | - Run `npm i` 35 | 36 | ## Table of Contents 37 | 38 | 1. [Add @angular/pwa schematic](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-1/README.md) 39 | 2. [Change web app manifest](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-2/README.md) 40 | 3. [Customize app icons and splash screens](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-3/README.md) 41 | 4. [Display A2HS on iOS](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-4/README.md) 42 | 5. [Add asset groups for app shell and icons](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-5/README.md) 43 | 6. [Add data group for conference data](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-6/README.md) 44 | 7. [Extend NGSW](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-7/README.md) 45 | 8. [Update PWA](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-8/README.md) 46 | 9. [Host on Firebase](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-9/README.md) 47 | 10. [Use an Android Emulator](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-10/README.md) 48 | 11. [Serve a secure local server](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-11/README.md) 49 | 12. [Test the A2HS functionality on Android](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-12/README.md) 50 | 13. [Add maskable icons](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-13/README.md) 51 | 14. [Subscribe to push notifications and manage permission](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-14/README.md) 52 | 15. [Send and receive push notifications](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-15/README.md) 53 | 16. [Save push subscriptions in a DB](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-16/README.md) 54 | 17. [Use an API from project Fugu](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-17/README.md) 55 | 18. [What's next?](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/final/README.md) 56 | 57 | Once you're ready with your local environment, you can start the workshop by navigation to the first step -> [Add @angular/pwa schematic](https://github.com/onderceylan/pwa-workshop-angular-firebase/blob/step-1/README.md). 58 | 59 | For questions, remarks and feedback; please contact me on [Twitter -> @onderceylan](https://twitter.com/onderceylan). 60 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "defaultProject": "app", 5 | "newProjectRoot": "projects", 6 | "projects": { 7 | "app": { 8 | "root": "", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "www", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | { 23 | "glob": "**/*", 24 | "input": "src/assets", 25 | "output": "assets" 26 | }, 27 | { 28 | "glob": "**/*.svg", 29 | "input": "node_modules/ionicons/dist/ionicons/svg", 30 | "output": "./svg" 31 | } 32 | ], 33 | "styles": [ 34 | { 35 | "input": "src/theme/variables.scss" 36 | }, 37 | { 38 | "input": "src/global.scss" 39 | } 40 | ], 41 | "scripts": [] 42 | }, 43 | "configurations": { 44 | "production": { 45 | "fileReplacements": [ 46 | { 47 | "src": "src/environments/environment.ts", 48 | "replaceWith": "src/environments/environment.prod.ts" 49 | } 50 | ], 51 | "optimization": true, 52 | "outputHashing": "all", 53 | "sourceMap": false, 54 | "extractCss": true, 55 | "namedChunks": false, 56 | "aot": true, 57 | "extractLicenses": true, 58 | "vendorChunk": true, 59 | "buildOptimizer": true, 60 | "budgets": [ 61 | { 62 | "type": "initial", 63 | "maximumWarning": "2mb", 64 | "maximumError": "5mb" 65 | } 66 | ] 67 | }, 68 | "ci": { 69 | "progress": false 70 | } 71 | } 72 | }, 73 | "serve": { 74 | "builder": "@angular-devkit/build-angular:dev-server", 75 | "options": { 76 | "browserTarget": "app:build" 77 | }, 78 | "configurations": { 79 | "production": { 80 | "browserTarget": "app:build:production" 81 | }, 82 | "ci": { 83 | "progress": false 84 | } 85 | } 86 | }, 87 | "extract-i18n": { 88 | "builder": "@angular-devkit/build-angular:extract-i18n", 89 | "options": { 90 | "browserTarget": "app:build" 91 | } 92 | }, 93 | "lint": { 94 | "builder": "@angular-devkit/build-angular:tslint", 95 | "options": { 96 | "tsConfig": [ 97 | "tsconfig.app.json", 98 | "tsconfig.spec.json" 99 | ], 100 | "exclude": [ 101 | "**/node_modules/**" 102 | ] 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "cli": { 109 | "defaultCollection": "@ionic/angular-toolkit" 110 | }, 111 | "schematics": { 112 | "@ionic/angular-toolkit:component": { 113 | "styleext": "scss" 114 | }, 115 | "@ionic/angular-toolkit:page": { 116 | "styleext": "scss" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwa-workshop-angular-firebase", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwa-workshop-angular-firebase", 3 | "version": "0.0.0", 4 | "description": "Workshop: Build a production ready PWA with Angular and Firebase", 5 | "license": "Apache-2.0", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "build:prod": "ng build --prod", 11 | "server": "npx superstatic www --port 8080 --host 127.0.0.1", 12 | "build:serve": "npm run build:prod", 13 | "postbuild:serve": "npm run server", 14 | "lint": "ng lint", 15 | "resources": "npx pwa-asset-generator ./src/assets/img/appicon.svg ./src/assets/pwa -b \"radial-gradient(circle farthest-corner at 1.3% 2.8%, rgba(239,249,249,1) 0%, rgba(182,199,226,1) 100%)\" -i ./src/index.html -m ./src/manifest.webmanifest -e false" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/onderceylan/pwa-workshop-angular-firebase.git" 20 | }, 21 | "private": true, 22 | "dependencies": { 23 | "@angular/common": "10.1.2", 24 | "@angular/core": "10.1.2", 25 | "@angular/forms": "10.1.2", 26 | "@angular/platform-browser": "10.1.2", 27 | "@angular/platform-browser-dynamic": "10.1.2", 28 | "@angular/router": "10.1.2", 29 | "@ionic/angular": "5.3.3", 30 | "@ionic/storage": "2.3.1", 31 | "core-js": "3.4.1", 32 | "rxjs": "6.5.5", 33 | "tslib": "2.0.0", 34 | "zone.js": "0.10.3" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "0.1001.2", 38 | "@angular/cli": "10.1.2", 39 | "@angular/compiler": "10.1.2", 40 | "@angular/compiler-cli": "10.1.2", 41 | "@angular/language-service": "10.1.2", 42 | "@ionic/angular-toolkit": "2.3.3", 43 | "@types/node": "12.11.1", 44 | "codelyzer": "6.0.0", 45 | "firebase-tools": "8.10.0", 46 | "pwa-asset-generator": "3.2.2", 47 | "superstatic": "7.0.0", 48 | "ts-node": "8.5.4", 49 | "tslint": "6.1.3", 50 | "typescript": "3.9.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { CheckTutorial } from './providers/check-tutorial.service'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | redirectTo: '/app/tabs/schedule', 9 | pathMatch: 'full' 10 | }, 11 | { 12 | path: 'account', 13 | loadChildren: () => import('./pages/account/account.module').then(m => m.AccountModule) 14 | }, 15 | { 16 | path: 'support', 17 | loadChildren: () => import('./pages/support/support.module').then(m => m.SupportModule) 18 | }, 19 | { 20 | path: 'login', 21 | loadChildren: () => import('./pages/login/login.module').then(m => m.LoginModule) 22 | }, 23 | { 24 | path: 'signup', 25 | loadChildren: () => import('./pages/signup/signup.module').then(m => m.SignUpModule) 26 | }, 27 | { 28 | path: 'app', 29 | loadChildren: () => import('./pages/tabs-page/tabs-page.module').then(m => m.TabsModule) 30 | }, 31 | { 32 | path: 'tutorial', 33 | loadChildren: () => import('./pages/tutorial/tutorial.module').then(m => m.TutorialModule), 34 | canLoad: [CheckTutorial] 35 | } 36 | ]; 37 | 38 | @NgModule({ 39 | imports: [RouterModule.forRoot(routes)], 40 | exports: [RouterModule] 41 | }) 42 | export class AppRoutingModule {} 43 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Menu 8 | 9 | 10 | 11 | 12 | 13 | 14 | Navigate 15 | 16 | 17 | 18 | 19 | 20 | {{p.title}} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Account 30 | 31 | 32 | 33 | 34 | 35 | 36 | Account 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Support 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Logout 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Account 64 | 65 | 66 | 67 | 68 | 69 | 70 | Login 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Support 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Signup 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Tutorial 97 | 98 | 99 | 100 | 101 | Show Tutorial 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Dark Theme 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | ion-item { 2 | --transition: unset; 3 | } 4 | 5 | .active { 6 | --color: var(--ion-color-primary); 7 | --background: rgba(var(--ion-color-primary-rgb), 0.12); 8 | --background-hover: rgba(var(--ion-color-primary-rgb), 0.16); 9 | --background-focused: rgba(var(--ion-color-primary-rgb), 0.24); 10 | 11 | ion-icon { 12 | color: inherit; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { MenuController } from '@ionic/angular'; 4 | import { Storage } from '@ionic/storage'; 5 | import { UserData } from './providers/user-data'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.scss'], 11 | encapsulation: ViewEncapsulation.None 12 | }) 13 | export class AppComponent implements OnInit { 14 | appPages = [ 15 | { 16 | title: 'Schedule', 17 | url: '/app/tabs/schedule', 18 | icon: 'calendar' 19 | }, 20 | { 21 | title: 'Speakers', 22 | url: '/app/tabs/speakers', 23 | icon: 'people' 24 | }, 25 | { 26 | title: 'Map', 27 | url: '/app/tabs/map', 28 | icon: 'map' 29 | }, 30 | { 31 | title: 'About', 32 | url: '/app/tabs/about', 33 | icon: 'information-circle' 34 | } 35 | ]; 36 | loggedIn = false; 37 | dark = false; 38 | 39 | constructor( 40 | private menu: MenuController, 41 | private router: Router, 42 | private storage: Storage, 43 | private userData: UserData, 44 | ) { } 45 | 46 | async ngOnInit() { 47 | this.checkLoginStatus(); 48 | this.listenForLoginEvents(); 49 | } 50 | 51 | checkLoginStatus() { 52 | return this.userData.isLoggedIn().then(loggedIn => { 53 | return this.updateLoggedInStatus(loggedIn); 54 | }); 55 | } 56 | 57 | updateLoggedInStatus(loggedIn: boolean) { 58 | setTimeout(() => { 59 | this.loggedIn = loggedIn; 60 | }, 300); 61 | } 62 | 63 | listenForLoginEvents() { 64 | window.addEventListener('user:login', () => { 65 | this.updateLoggedInStatus(true); 66 | }); 67 | 68 | window.addEventListener('user:signup', () => { 69 | this.updateLoggedInStatus(true); 70 | }); 71 | 72 | window.addEventListener('user:logout', () => { 73 | this.updateLoggedInStatus(false); 74 | }); 75 | } 76 | 77 | logout() { 78 | this.userData.logout().then(() => { 79 | return this.router.navigateByUrl('/app/tabs/schedule'); 80 | }); 81 | } 82 | 83 | openTutorial() { 84 | this.menu.enable(false); 85 | this.storage.set('ion_did_tutorial', false); 86 | this.router.navigateByUrl('/tutorial'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { IonicModule } from '@ionic/angular'; 6 | import { IonicStorageModule } from '@ionic/storage'; 7 | import { AppRoutingModule } from './app-routing.module'; 8 | import { AppComponent } from './app.component'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | BrowserModule, 13 | AppRoutingModule, 14 | HttpClientModule, 15 | FormsModule, 16 | IonicModule.forRoot(), 17 | IonicStorageModule.forRoot(), 18 | ], 19 | declarations: [AppComponent], 20 | bootstrap: [AppComponent] 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/app/app.scss -------------------------------------------------------------------------------- /src/app/interfaces/user-options.ts: -------------------------------------------------------------------------------- 1 | export interface UserOptions { 2 | username: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/pages/about-popover/about-popover.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { PopoverController } from '@ionic/angular'; 3 | 4 | @Component({ 5 | template: ` 6 | 7 | 8 | Learn Ionic 9 | 10 | 11 | Documentation 12 | 13 | 14 | Showcase 15 | 16 | 17 | GitHub Repo 18 | 19 | 20 | Support 21 | 22 | 23 | ` 24 | }) 25 | export class PopoverPage { 26 | constructor(public popoverCtrl: PopoverController) {} 27 | 28 | support() { 29 | // this.app.getRootNavs()[0].push('/support'); 30 | this.popoverCtrl.dismiss(); 31 | } 32 | 33 | close(url: string) { 34 | window.open(url, '_blank'); 35 | this.popoverCtrl.dismiss(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/pages/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AboutPage } from './about'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: AboutPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AboutPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/about/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | About 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | ionic logo 18 |
19 |
20 |

Ionic Conference

21 | 22 | 23 | 24 | 25 | Date 26 | 27 | 28 | 29 | 30 | 31 | Location 32 | 33 | Madison, WI 34 | Austin, TX 35 | Chicago, IL 36 | Seattle, WA 37 | 38 | 39 | 40 | 41 |

42 | The Ionic Conference is a one-day conference featuring talks from the Ionic team. It is focused on Ionic applications being 43 | built with Ionic Framework. This includes migrating apps to the latest Ionic Framework, Angular concepts, Webpack, Sass, and many 44 | other technologies used in Ionic Framework. Tickets are completely sold out, and we’re expecting more than 1000 developers 45 | – making this the largest Ionic conference ever! 46 |

47 |
48 |
49 | -------------------------------------------------------------------------------- /src/app/pages/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { IonicModule } from '@ionic/angular'; 5 | import { AboutPage } from './about'; 6 | import { PopoverPage } from '../about-popover/about-popover'; 7 | import { AboutPageRoutingModule } from './about-routing.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | FormsModule, 13 | IonicModule, 14 | AboutPageRoutingModule 15 | ], 16 | declarations: [AboutPage, PopoverPage], 17 | entryComponents: [PopoverPage], 18 | bootstrap: [AboutPage], 19 | }) 20 | export class AboutModule {} 21 | -------------------------------------------------------------------------------- /src/app/pages/about/about.scss: -------------------------------------------------------------------------------- 1 | .about-header { 2 | background-color: #222; 3 | padding: 16px; 4 | width: 100%; 5 | height: 30%; 6 | text-align: center; 7 | } 8 | 9 | .about-header img { 10 | max-height: 100%; 11 | } 12 | 13 | .about-info p { 14 | color: var(--ion-color-dark); 15 | text-align: left; 16 | } 17 | 18 | .about-info ion-icon { 19 | margin-inline-end: 32px; 20 | } 21 | 22 | .ios .about-info { 23 | text-align: center; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/pages/about/about.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { PopoverController } from '@ionic/angular'; 3 | import { PopoverPage } from '../about-popover/about-popover'; 4 | 5 | @Component({ 6 | selector: 'page-about', 7 | templateUrl: 'about.html', 8 | styleUrls: ['./about.scss'], 9 | }) 10 | export class AboutPage { 11 | conferenceDate = '2047-05-17'; 12 | 13 | constructor(public popoverCtrl: PopoverController) { } 14 | 15 | async presentPopover(event: Event) { 16 | const popover = await this.popoverCtrl.create({ 17 | component: PopoverPage, 18 | event 19 | }); 20 | await popover.present(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/account/account-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AccountPage } from './account'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: AccountPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AccountPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/account/account.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Account 7 | 8 | 9 | 10 | 11 |
12 | avatar 13 |

{{username}}

14 | 15 | 16 | Update Picture 17 | Change Username 18 | Change Password 19 | Support 20 | Logout 21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/pages/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { AccountPage } from './account'; 5 | import { AccountPageRoutingModule } from './account-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | IonicModule, 11 | AccountPageRoutingModule 12 | ], 13 | declarations: [ 14 | AccountPage, 15 | ] 16 | }) 17 | export class AccountModule { } 18 | -------------------------------------------------------------------------------- /src/app/pages/account/account.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 140px; 3 | border-radius: 50%; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/pages/account/account.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AlertController } from '@ionic/angular'; 4 | import { UserData } from '../../providers/user-data'; 5 | 6 | @Component({ 7 | selector: 'page-account', 8 | templateUrl: 'account.html', 9 | styleUrls: ['./account.scss'], 10 | }) 11 | export class AccountPage implements AfterViewInit { 12 | username: string; 13 | 14 | constructor( 15 | public alertCtrl: AlertController, 16 | public router: Router, 17 | public userData: UserData 18 | ) { } 19 | 20 | ngAfterViewInit() { 21 | this.getUsername(); 22 | } 23 | 24 | updatePicture() { 25 | console.log('Clicked to update picture'); 26 | } 27 | 28 | // Present an alert with the current username populated 29 | // clicking OK will update the username and display it 30 | // clicking Cancel will close the alert and do nothing 31 | async changeUsername() { 32 | const alert = await this.alertCtrl.create({ 33 | header: 'Change Username', 34 | buttons: [ 35 | 'Cancel', 36 | { 37 | text: 'Ok', 38 | handler: (data: any) => { 39 | this.userData.setUsername(data.username); 40 | this.getUsername(); 41 | } 42 | } 43 | ], 44 | inputs: [ 45 | { 46 | type: 'text', 47 | name: 'username', 48 | value: this.username, 49 | placeholder: 'username' 50 | } 51 | ] 52 | }); 53 | await alert.present(); 54 | } 55 | 56 | getUsername() { 57 | this.userData.getUsername().then((username) => { 58 | this.username = username; 59 | }); 60 | } 61 | 62 | changePassword() { 63 | console.log('Clicked to change password'); 64 | } 65 | 66 | logout() { 67 | this.userData.logout(); 68 | this.router.navigateByUrl('/login'); 69 | } 70 | 71 | support() { 72 | this.router.navigateByUrl('/support'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/pages/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { LoginPage } from './login'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: LoginPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class LoginPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/login/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Login 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 | 18 | 19 | Username 20 | 22 | 23 | 24 | 25 | 26 |

27 | Username is required 28 |

29 |
30 | 31 | 32 | Password 33 | 34 | 35 | 36 | 37 | 38 |

39 | Password is required 40 |

41 |
42 |
43 | 44 | 45 | 46 | Login 47 | 48 | 49 | Signup 50 | 51 | 52 |
53 | 54 |
55 | -------------------------------------------------------------------------------- /src/app/pages/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { IonicModule } from '@ionic/angular'; 5 | import { LoginPage } from './login'; 6 | import { LoginPageRoutingModule } from './login-routing.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | FormsModule, 12 | IonicModule, 13 | LoginPageRoutingModule 14 | ], 15 | declarations: [ 16 | LoginPage, 17 | ] 18 | }) 19 | export class LoginModule { } 20 | -------------------------------------------------------------------------------- /src/app/pages/login/login.scss: -------------------------------------------------------------------------------- 1 | .login-logo { 2 | padding: 20px 0; 3 | min-height: 200px; 4 | text-align: center; 5 | } 6 | 7 | .login-logo img { 8 | max-width: 150px; 9 | } 10 | 11 | .list { 12 | margin-bottom: 0; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/login/login.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { UserData } from '../../providers/user-data'; 5 | import { UserOptions } from '../../interfaces/user-options'; 6 | 7 | @Component({ 8 | selector: 'page-login', 9 | templateUrl: 'login.html', 10 | styleUrls: ['./login.scss'], 11 | }) 12 | export class LoginPage { 13 | login: UserOptions = { username: '', password: '' }; 14 | submitted = false; 15 | 16 | constructor( 17 | public userData: UserData, 18 | public router: Router 19 | ) { } 20 | 21 | onLogin(form: NgForm) { 22 | this.submitted = true; 23 | 24 | if (form.valid) { 25 | this.userData.login(this.login.username); 26 | this.router.navigateByUrl('/app/tabs/schedule'); 27 | } 28 | } 29 | 30 | onSignup() { 31 | this.router.navigateByUrl('/signup'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/pages/map/map-dark-style.js: -------------------------------------------------------------------------------- 1 | export const darkStyle = [ 2 | { 3 | "elementType": "geometry", 4 | "stylers": [ 5 | { 6 | "color": "#242f3e" 7 | } 8 | ] 9 | }, 10 | { 11 | "elementType": "labels.text.fill", 12 | "stylers": [ 13 | { 14 | "color": "#746855" 15 | } 16 | ] 17 | }, 18 | { 19 | "elementType": "labels.text.stroke", 20 | "stylers": [ 21 | { 22 | "color": "#242f3e" 23 | } 24 | ] 25 | }, 26 | { 27 | "featureType": "administrative.locality", 28 | "elementType": "labels.text.fill", 29 | "stylers": [ 30 | { 31 | "color": "#d59563" 32 | } 33 | ] 34 | }, 35 | { 36 | "featureType": "poi", 37 | "elementType": "labels.text.fill", 38 | "stylers": [ 39 | { 40 | "color": "#d59563" 41 | } 42 | ] 43 | }, 44 | { 45 | "featureType": "poi.park", 46 | "elementType": "geometry", 47 | "stylers": [ 48 | { 49 | "color": "#263c3f" 50 | } 51 | ] 52 | }, 53 | { 54 | "featureType": "poi.park", 55 | "elementType": "labels.text.fill", 56 | "stylers": [ 57 | { 58 | "color": "#6b9a76" 59 | } 60 | ] 61 | }, 62 | { 63 | "featureType": "road", 64 | "elementType": "geometry", 65 | "stylers": [ 66 | { 67 | "color": "#38414e" 68 | } 69 | ] 70 | }, 71 | { 72 | "featureType": "road", 73 | "elementType": "geometry.stroke", 74 | "stylers": [ 75 | { 76 | "color": "#212a37" 77 | } 78 | ] 79 | }, 80 | { 81 | "featureType": "road", 82 | "elementType": "labels.text.fill", 83 | "stylers": [ 84 | { 85 | "color": "#9ca5b3" 86 | } 87 | ] 88 | }, 89 | { 90 | "featureType": "road.highway", 91 | "elementType": "geometry", 92 | "stylers": [ 93 | { 94 | "color": "#746855" 95 | } 96 | ] 97 | }, 98 | { 99 | "featureType": "road.highway", 100 | "elementType": "geometry.stroke", 101 | "stylers": [ 102 | { 103 | "color": "#1f2835" 104 | } 105 | ] 106 | }, 107 | { 108 | "featureType": "road.highway", 109 | "elementType": "labels.text.fill", 110 | "stylers": [ 111 | { 112 | "color": "#f3d19c" 113 | } 114 | ] 115 | }, 116 | { 117 | "featureType": "transit", 118 | "elementType": "geometry", 119 | "stylers": [ 120 | { 121 | "color": "#2f3948" 122 | } 123 | ] 124 | }, 125 | { 126 | "featureType": "transit.station", 127 | "elementType": "labels.text.fill", 128 | "stylers": [ 129 | { 130 | "color": "#d59563" 131 | } 132 | ] 133 | }, 134 | { 135 | "featureType": "water", 136 | "elementType": "geometry", 137 | "stylers": [ 138 | { 139 | "color": "#17263c" 140 | } 141 | ] 142 | }, 143 | { 144 | "featureType": "water", 145 | "elementType": "labels.text.fill", 146 | "stylers": [ 147 | { 148 | "color": "#515c6d" 149 | } 150 | ] 151 | }, 152 | { 153 | "featureType": "water", 154 | "elementType": "labels.text.stroke", 155 | "stylers": [ 156 | { 157 | "color": "#17263c" 158 | } 159 | ] 160 | } 161 | ] -------------------------------------------------------------------------------- /src/app/pages/map/map-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { MapPage } from './map'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: MapPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class MapPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/map/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Map 7 | 8 | 9 | 10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /src/app/pages/map/map.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { MapPage } from './map'; 5 | import { MapPageRoutingModule } from './map-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | IonicModule, 11 | MapPageRoutingModule 12 | ], 13 | declarations: [ 14 | MapPage, 15 | ] 16 | }) 17 | export class MapModule { } 18 | -------------------------------------------------------------------------------- /src/app/pages/map/map.scss: -------------------------------------------------------------------------------- 1 | .map-canvas { 2 | position: absolute; 3 | 4 | height: 100%; 5 | width: 100%; 6 | 7 | background-color: transparent; 8 | 9 | opacity: 0; 10 | transition: opacity 150ms ease-in; 11 | } 12 | 13 | .show-map { 14 | opacity: 1; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pages/map/map.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Inject, ViewChild, AfterViewInit } from '@angular/core'; 2 | import { ConferenceData } from '../../providers/conference-data'; 3 | import { Platform } from '@ionic/angular'; 4 | import { DOCUMENT} from '@angular/common'; 5 | 6 | import { darkStyle } from './map-dark-style'; 7 | 8 | @Component({ 9 | selector: 'page-map', 10 | templateUrl: 'map.html', 11 | styleUrls: ['./map.scss'] 12 | }) 13 | export class MapPage implements AfterViewInit { 14 | @ViewChild('mapCanvas', { static: true }) mapElement: ElementRef; 15 | 16 | constructor( 17 | @Inject(DOCUMENT) private doc: Document, 18 | public confData: ConferenceData, 19 | public platform: Platform) {} 20 | 21 | async ngAfterViewInit() { 22 | const appEl = this.doc.querySelector('ion-app'); 23 | let isDark = false; 24 | let style = []; 25 | if (appEl.classList.contains('dark-theme')) { 26 | style = darkStyle; 27 | } 28 | 29 | const googleMaps = await getGoogleMaps( 30 | 'AIzaSyB8pf6ZdFQj5qw7rc_HSGrhUwQKfIe9ICw' 31 | ); 32 | 33 | let map; 34 | 35 | this.confData.getMap().subscribe((mapData: any) => { 36 | const mapEle = this.mapElement.nativeElement; 37 | 38 | map = new googleMaps.Map(mapEle, { 39 | center: mapData.find((d: any) => d.center), 40 | zoom: 16, 41 | styles: style 42 | }); 43 | 44 | mapData.forEach((markerData: any) => { 45 | const infoWindow = new googleMaps.InfoWindow({ 46 | content: `
${markerData.name}
` 47 | }); 48 | 49 | const marker = new googleMaps.Marker({ 50 | position: markerData, 51 | map, 52 | title: markerData.name 53 | }); 54 | 55 | marker.addListener('click', () => { 56 | infoWindow.open(map, marker); 57 | }); 58 | }); 59 | 60 | googleMaps.event.addListenerOnce(map, 'idle', () => { 61 | mapEle.classList.add('show-map'); 62 | }); 63 | }); 64 | 65 | const observer = new MutationObserver(function (mutations) { 66 | mutations.forEach((mutation) => { 67 | if (mutation.attributeName === 'class') { 68 | const el = mutation.target as HTMLElement; 69 | isDark = el.classList.contains('dark-theme'); 70 | if (map && isDark) { 71 | map.setOptions({styles: darkStyle}); 72 | } else if (map) { 73 | map.setOptions({styles: []}); 74 | } 75 | } 76 | }); 77 | }); 78 | observer.observe(appEl, { 79 | attributes: true 80 | }); 81 | } 82 | } 83 | 84 | function getGoogleMaps(apiKey: string): Promise { 85 | const win = window as any; 86 | const googleModule = win.google; 87 | if (googleModule && googleModule.maps) { 88 | return Promise.resolve(googleModule.maps); 89 | } 90 | 91 | return new Promise((resolve, reject) => { 92 | const script = document.createElement('script'); 93 | script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&v=3.41`; 94 | script.async = true; 95 | script.defer = true; 96 | document.body.appendChild(script); 97 | script.onload = () => { 98 | const googleModule2 = win.google; 99 | if (googleModule2 && googleModule2.maps) { 100 | resolve(googleModule2.maps); 101 | } else { 102 | reject('google maps not available'); 103 | } 104 | }; 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /src/app/pages/schedule-filter/schedule-filter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cancel 5 | Reset 6 | 7 | 8 | 9 | Filter Sessions 10 | 11 | 12 | 13 | Done 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Tracks 22 | 23 | 24 | 25 | {{track.name}} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Deselect All 37 | 38 | 39 | Select All 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/app/pages/schedule-filter/schedule-filter.scss: -------------------------------------------------------------------------------- 1 | .md { 2 | ion-toolbar ion-button { 3 | text-transform: capitalize; 4 | letter-spacing: 0; 5 | } 6 | 7 | ion-checkbox { 8 | --background-checked: transparent; 9 | --border-color: transparent; 10 | --border-color-checked: transparent; 11 | --checkmark-color: var(--ion-color-primary); 12 | } 13 | 14 | ion-list { 15 | background: inherit; 16 | } 17 | } 18 | 19 | .ios { 20 | // TODO this needs to be added to Ionic: 21 | // https://github.com/ionic-team/ionic/pull/16574 22 | ion-list-header { 23 | height: 50px; 24 | font-size: 22px; 25 | letter-spacing: 0; 26 | text-transform: capitalize; 27 | } 28 | 29 | ion-label { 30 | color: var(--ion-color-primary); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/pages/schedule-filter/schedule-filter.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component } from '@angular/core'; 2 | import { Config, ModalController, NavParams } from '@ionic/angular'; 3 | import { ConferenceData } from '../../providers/conference-data'; 4 | 5 | @Component({ 6 | selector: 'page-schedule-filter', 7 | templateUrl: 'schedule-filter.html', 8 | styleUrls: ['./schedule-filter.scss'], 9 | }) 10 | export class ScheduleFilterPage implements AfterViewInit { 11 | ios: boolean; 12 | 13 | tracks: {name: string, icon: string, isChecked: boolean}[] = []; 14 | 15 | constructor( 16 | public confData: ConferenceData, 17 | private config: Config, 18 | public modalCtrl: ModalController, 19 | public navParams: NavParams 20 | ) { } 21 | 22 | ionViewWillEnter() { 23 | this.ios = this.config.get('mode') === `ios`; 24 | } 25 | 26 | // TODO use the ionViewDidEnter event 27 | ngAfterViewInit() { 28 | // passed in array of track names that should be excluded (unchecked) 29 | const excludedTrackNames = this.navParams.get('excludedTracks'); 30 | 31 | this.confData.getTracks().subscribe((tracks: any[]) => { 32 | tracks.forEach(track => { 33 | this.tracks.push({ 34 | name: track.name, 35 | icon: track.icon, 36 | isChecked: (excludedTrackNames.indexOf(track.name) === -1) 37 | }); 38 | }); 39 | }); 40 | } 41 | 42 | selectAll(check: boolean) { 43 | // set all to checked or unchecked 44 | this.tracks.forEach(track => { 45 | track.isChecked = check; 46 | }); 47 | } 48 | 49 | applyFilters() { 50 | // Pass back a new array of track names to exclude 51 | const excludedTrackNames = this.tracks.filter(c => !c.isChecked).map(c => c.name); 52 | this.dismiss(excludedTrackNames); 53 | } 54 | 55 | dismiss(data?: any) { 56 | // using the injected ModalController this page 57 | // can "dismiss" itself and pass back data 58 | this.modalCtrl.dismiss(data); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/pages/schedule/schedule-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SchedulePage } from './schedule'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SchedulePage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SchedulePageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/schedule/schedule.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Sessions 21 | 22 | 23 | 24 | Filter 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{group.time}} 43 | 44 | 45 | 46 | 48 | 49 | 50 |

{{session.name}}

51 |

52 | {{session.timeStart}} — {{session.timeEnd}}: {{session.location}} 53 |

54 |
55 |
56 | 57 | 58 | Favorite 59 | 60 | 62 | Remove 63 | 64 | 65 |
66 |
67 |
68 | 69 | 70 | No Sessions Found 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
93 | -------------------------------------------------------------------------------- /src/app/pages/schedule/schedule.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { IonicModule } from '@ionic/angular'; 5 | import { SchedulePage } from './schedule'; 6 | import { ScheduleFilterPage } from '../schedule-filter/schedule-filter'; 7 | import { SchedulePageRoutingModule } from './schedule-routing.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | FormsModule, 13 | IonicModule, 14 | SchedulePageRoutingModule 15 | ], 16 | declarations: [ 17 | SchedulePage, 18 | ScheduleFilterPage 19 | ], 20 | entryComponents: [ 21 | ScheduleFilterPage 22 | ] 23 | }) 24 | export class ScheduleModule { } 25 | -------------------------------------------------------------------------------- /src/app/pages/schedule/schedule.scss: -------------------------------------------------------------------------------- 1 | $categories: ( 2 | ionic: var(--ion-color-primary), 3 | angular: #ac282b, 4 | communication: #8e8d93, 5 | tooling: #fe4c52, 6 | services: #fd8b2d, 7 | design: #fed035, 8 | workshop: #69bb7b, 9 | food: #3bc7c4, 10 | documentation: #b16be3, 11 | navigation: #6600cc 12 | ); 13 | 14 | @each $track, $value in map-remove($categories) { 15 | ion-item-sliding[track='#{$track}'] ion-label { 16 | border-left: 2px solid $value; 17 | padding-left: 10px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/pages/schedule/schedule.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { 4 | AlertController, 5 | IonList, 6 | LoadingController, 7 | ModalController, 8 | ToastController, 9 | Config, 10 | IonItemSliding, 11 | IonFab 12 | } from '@ionic/angular'; 13 | import { ScheduleFilterPage } from '../schedule-filter/schedule-filter'; 14 | import { ConferenceData } from '../../providers/conference-data'; 15 | import { UserData } from '../../providers/user-data'; 16 | 17 | @Component({ 18 | selector: 'page-schedule', 19 | templateUrl: 'schedule.html', 20 | styleUrls: ['./schedule.scss'], 21 | }) 22 | export class SchedulePage implements OnInit { 23 | // Gets a reference to the list element 24 | @ViewChild('scheduleList', { static: true }) scheduleList: IonList; 25 | 26 | ios: boolean; 27 | dayIndex = 0; 28 | queryText = ''; 29 | segment = 'all'; 30 | excludeTracks: any = []; 31 | shownSessions: any = []; 32 | groups: any = []; 33 | confDate: string; 34 | 35 | constructor( 36 | public alertCtrl: AlertController, 37 | public confData: ConferenceData, 38 | public loadingCtrl: LoadingController, 39 | public modalCtrl: ModalController, 40 | public router: Router, 41 | public toastCtrl: ToastController, 42 | public user: UserData, 43 | public config: Config 44 | ) { } 45 | 46 | ngOnInit() { 47 | this.updateSchedule(); 48 | 49 | this.ios = this.config.get('mode') === 'ios'; 50 | } 51 | 52 | updateSchedule() { 53 | // Close any open sliding items when the schedule updates 54 | if (this.scheduleList) { 55 | this.scheduleList.closeSlidingItems(); 56 | } 57 | 58 | this.confData.getTimeline(this.dayIndex, this.queryText, this.excludeTracks, this.segment).subscribe((data: any) => { 59 | this.shownSessions = data.shownSessions; 60 | this.groups = data.groups; 61 | }); 62 | } 63 | 64 | async presentFilter() { 65 | const modal = await this.modalCtrl.create({ 66 | component: ScheduleFilterPage, 67 | componentProps: { excludedTracks: this.excludeTracks } 68 | }); 69 | await modal.present(); 70 | 71 | const { data } = await modal.onWillDismiss(); 72 | if (data) { 73 | this.excludeTracks = data; 74 | this.updateSchedule(); 75 | } 76 | } 77 | 78 | async addFavorite(slidingItem: IonItemSliding, sessionData: any) { 79 | if (this.user.hasFavorite(sessionData.name)) { 80 | // woops, they already favorited it! What shall we do!? 81 | // prompt them to remove it 82 | this.removeFavorite(slidingItem, sessionData, 'Favorite already added'); 83 | } else { 84 | // remember this session as a user favorite 85 | this.user.addFavorite(sessionData.name); 86 | 87 | // create an alert instance 88 | const alert = await this.alertCtrl.create({ 89 | header: 'Favorite Added', 90 | buttons: [{ 91 | text: 'OK', 92 | handler: () => { 93 | // close the sliding item 94 | slidingItem.close(); 95 | } 96 | }] 97 | }); 98 | // now present the alert on top of all other content 99 | await alert.present(); 100 | } 101 | 102 | } 103 | 104 | async removeFavorite(slidingItem: IonItemSliding, sessionData: any, title: string) { 105 | const alert = await this.alertCtrl.create({ 106 | header: title, 107 | message: 'Would you like to remove this session from your favorites?', 108 | buttons: [ 109 | { 110 | text: 'Cancel', 111 | handler: () => { 112 | // they clicked the cancel button, do not remove the session 113 | // close the sliding item and hide the option buttons 114 | slidingItem.close(); 115 | } 116 | }, 117 | { 118 | text: 'Remove', 119 | handler: () => { 120 | // they want to remove this session from their favorites 121 | this.user.removeFavorite(sessionData.name); 122 | this.updateSchedule(); 123 | 124 | // close the sliding item and hide the option buttons 125 | slidingItem.close(); 126 | } 127 | } 128 | ] 129 | }); 130 | // now present the alert on top of all other content 131 | await alert.present(); 132 | } 133 | 134 | async openSocial(network: string, fab: IonFab) { 135 | const loading = await this.loadingCtrl.create({ 136 | message: `Posting to ${network}`, 137 | duration: (Math.random() * 1000) + 500 138 | }); 139 | await loading.present(); 140 | await loading.onWillDismiss(); 141 | fab.close(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/app/pages/session-detail/session-detail-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SessionDetailPage } from './session-detail'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SessionDetailPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SessionDetailPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/session-detail/session-detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

{{session.name}}

21 | {{track}} 22 |

{{session.description}}

23 | 24 | {{session.timeStart}} – {{session.timeEnd}} 25 |
{{session.location}} 26 |
27 |
28 | 29 | 30 | 31 | Watch 32 | 33 | 34 | Add to Calendar 35 | 36 | 37 | Mark as Unwatched 38 | 39 | 40 | Download Video 41 | 42 | 43 | 44 | Leave Feedback 45 | 46 | 47 |
48 | -------------------------------------------------------------------------------- /src/app/pages/session-detail/session-detail.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SessionDetailPage } from './session-detail'; 4 | import { SessionDetailPageRoutingModule } from './session-detail-routing.module'; 5 | import { IonicModule } from '@ionic/angular'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | IonicModule, 11 | SessionDetailPageRoutingModule 12 | ], 13 | declarations: [ 14 | SessionDetailPage, 15 | ] 16 | }) 17 | export class SessionDetailModule { } 18 | -------------------------------------------------------------------------------- /src/app/pages/session-detail/session-detail.scss: -------------------------------------------------------------------------------- 1 | .session-track-ionic { 2 | color: var(--ion-color-primary); 3 | } 4 | 5 | .session-track-angular { 6 | color: var(--ion-color-angular); 7 | } 8 | 9 | .session-track-communication { 10 | color: var(--ion-color-communication); 11 | } 12 | 13 | .session-track-tooling { 14 | color: var(--ion-color-tooling); 15 | } 16 | 17 | .session-track-services { 18 | color: var(--ion-color-services); 19 | } 20 | 21 | .session-track-design { 22 | color: var(--ion-color-design); 23 | } 24 | 25 | .session-track-workshop { 26 | color: var(--ion-color-workshop); 27 | } 28 | 29 | .session-track-food { 30 | color: var(--ion-color-food); 31 | } 32 | 33 | .session-track-documentation { 34 | color: var(--ion-color-documentation); 35 | } 36 | 37 | .session-track-navigation { 38 | color: var(--ion-color-navigation); 39 | } 40 | 41 | // Favorite Icon 42 | // -------------------------------------------------------- 43 | 44 | .show-favorite { 45 | position: relative; 46 | } 47 | 48 | .icon-heart-empty, 49 | .icon-heart { 50 | --border-radius: 50%; 51 | --padding-start: 0; 52 | --padding-end: 0; 53 | 54 | position: absolute; 55 | top: 5px; 56 | right: 5px; 57 | 58 | width: 48px; 59 | height: 48px; 60 | 61 | font-size: 16px; 62 | 63 | transition: transform 300ms ease; 64 | } 65 | 66 | .icon-heart-empty { 67 | transform: scale(1); 68 | } 69 | 70 | .icon-heart { 71 | transform: scale(0); 72 | } 73 | 74 | .show-favorite .icon-heart { 75 | transform: scale(1); 76 | } 77 | 78 | .show-favorite .icon-heart-empty { 79 | transform: scale(0); 80 | } 81 | 82 | h1 { 83 | margin: 0; 84 | } 85 | -------------------------------------------------------------------------------- /src/app/pages/session-detail/session-detail.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { ConferenceData } from '../../providers/conference-data'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { UserData } from '../../providers/user-data'; 6 | 7 | @Component({ 8 | selector: 'page-session-detail', 9 | styleUrls: ['./session-detail.scss'], 10 | templateUrl: 'session-detail.html' 11 | }) 12 | export class SessionDetailPage { 13 | session: any; 14 | isFavorite = false; 15 | defaultHref = ''; 16 | 17 | constructor( 18 | private dataProvider: ConferenceData, 19 | private userProvider: UserData, 20 | private route: ActivatedRoute 21 | ) { } 22 | 23 | ionViewWillEnter() { 24 | this.dataProvider.load().subscribe((data: any) => { 25 | if (data && data.schedule && data.schedule[0] && data.schedule[0].groups) { 26 | const sessionId = this.route.snapshot.paramMap.get('sessionId'); 27 | for (const group of data.schedule[0].groups) { 28 | if (group && group.sessions) { 29 | for (const session of group.sessions) { 30 | if (session && session.id === sessionId) { 31 | this.session = session; 32 | 33 | this.isFavorite = this.userProvider.hasFavorite( 34 | this.session.name 35 | ); 36 | 37 | break; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | }); 44 | } 45 | 46 | ionViewDidEnter() { 47 | this.defaultHref = `/app/tabs/schedule`; 48 | } 49 | 50 | sessionClick(item: string) { 51 | console.log('Clicked', item); 52 | } 53 | 54 | toggleFavorite() { 55 | if (this.userProvider.hasFavorite(this.session.name)) { 56 | this.userProvider.removeFavorite(this.session.name); 57 | this.isFavorite = false; 58 | } else { 59 | this.userProvider.addFavorite(this.session.name); 60 | this.isFavorite = true; 61 | } 62 | } 63 | 64 | shareSession() { 65 | console.log('Clicked share session'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/pages/signup/signup-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SignupPage } from './signup'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SignupPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SignupPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/signup/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Signup 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 | 18 | 19 | Username 20 | 21 | 22 | 23 | 24 |

25 | Username is required 26 |

27 |
28 | 29 | 30 | Password 31 | 32 | 33 | 34 | 35 |

36 | Password is required 37 |

38 |
39 |
40 | 41 |
42 | Create 43 |
44 |
45 | 46 |
47 | -------------------------------------------------------------------------------- /src/app/pages/signup/signup.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { IonicModule } from '@ionic/angular'; 5 | import { SignupPage } from './signup'; 6 | import { SignupPageRoutingModule } from './signup-routing.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | FormsModule, 12 | IonicModule, 13 | SignupPageRoutingModule 14 | ], 15 | declarations: [ 16 | SignupPage, 17 | ] 18 | }) 19 | export class SignUpModule { } 20 | -------------------------------------------------------------------------------- /src/app/pages/signup/signup.scss: -------------------------------------------------------------------------------- 1 | .signup-logo { 2 | padding: 20px 0; 3 | min-height: 200px; 4 | text-align: center; 5 | } 6 | 7 | .signup-logo img { 8 | max-width: 150px; 9 | } 10 | 11 | .list { 12 | margin-bottom: 0; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/signup/signup.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { UserData } from '../../providers/user-data'; 5 | import { UserOptions } from '../../interfaces/user-options'; 6 | 7 | @Component({ 8 | selector: 'page-signup', 9 | templateUrl: 'signup.html', 10 | styleUrls: ['./signup.scss'], 11 | }) 12 | export class SignupPage { 13 | signup: UserOptions = { username: '', password: '' }; 14 | submitted = false; 15 | 16 | constructor( 17 | public router: Router, 18 | public userData: UserData 19 | ) {} 20 | 21 | onSignup(form: NgForm) { 22 | this.submitted = true; 23 | 24 | if (form.valid) { 25 | this.userData.signup(this.signup.username); 26 | this.router.navigateByUrl('/app/tabs/schedule'); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/pages/speaker-detail/speaker-detail-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SpeakerDetailPage } from './speaker-detail'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SpeakerDetailPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SpeakerDetailPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/speaker-detail/speaker-detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{speaker?.name}} 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |

{{speaker?.about}}

27 |
28 | -------------------------------------------------------------------------------- /src/app/pages/speaker-detail/speaker-detail.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SpeakerDetailPage } from './speaker-detail'; 4 | import { SpeakerDetailPageRoutingModule } from './speaker-detail-routing.module'; 5 | import { IonicModule } from '@ionic/angular'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | IonicModule, 11 | SpeakerDetailPageRoutingModule 12 | ], 13 | declarations: [ 14 | SpeakerDetailPage, 15 | ] 16 | }) 17 | export class SpeakerDetailModule { } 18 | -------------------------------------------------------------------------------- /src/app/pages/speaker-detail/speaker-detail.scss: -------------------------------------------------------------------------------- 1 | .speaker-detail img { 2 | max-width: 140px; 3 | border-radius: 50%; 4 | } 5 | 6 | .speaker-detail p { 7 | color: #60646b; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/pages/speaker-detail/speaker-detail.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { ConferenceData } from '../../providers/conference-data'; 4 | 5 | @Component({ 6 | selector: 'page-speaker-detail', 7 | templateUrl: 'speaker-detail.html', 8 | styleUrls: ['./speaker-detail.scss'], 9 | }) 10 | export class SpeakerDetailPage { 11 | speaker: any; 12 | 13 | constructor( 14 | private dataProvider: ConferenceData, 15 | private router: Router, 16 | private route: ActivatedRoute 17 | ) {} 18 | 19 | ionViewWillEnter() { 20 | this.dataProvider.load().subscribe((data: any) => { 21 | const speakerId = this.route.snapshot.paramMap.get('speakerId'); 22 | if (data && data.speakers) { 23 | for (const speaker of data.speakers) { 24 | if (speaker && speaker.id === speakerId) { 25 | this.speaker = speaker; 26 | break; 27 | } 28 | } 29 | } 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/pages/speaker-list/speaker-list-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SpeakerListPage } from './speaker-list'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SpeakerListPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SpeakerListPageRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/pages/speaker-list/speaker-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Speakers 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{speaker.name}} 21 | 22 | 23 | 24 | 25 | 26 | 27 |

{{session.name}}

28 |
29 | 30 | 31 |

About {{speaker.name}}

32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | Tweet 41 | 42 | 43 | 44 | 45 | 46 | Share 47 | 48 | 49 | 50 | 51 | 52 | Contact 53 | 54 | 55 | 56 |
57 |
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /src/app/pages/speaker-list/speaker-list.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { SpeakerListPage } from './speaker-list'; 5 | import { SpeakerListPageRoutingModule } from './speaker-list-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | IonicModule, 11 | SpeakerListPageRoutingModule 12 | ], 13 | declarations: [SpeakerListPage], 14 | }) 15 | export class SpeakerListModule {} 16 | -------------------------------------------------------------------------------- /src/app/pages/speaker-list/speaker-list.scss: -------------------------------------------------------------------------------- 1 | .scroll { 2 | background: #ededed; 3 | } 4 | 5 | .speaker-card { 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .speaker-card ion-card-header { 12 | padding: 0; 13 | } 14 | 15 | .speaker-card ion-card-header .item { 16 | padding: 4px 16px; 17 | } 18 | 19 | .speaker-card ion-card-content { 20 | flex: 1 1 auto; 21 | 22 | padding: 0; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/pages/speaker-list/speaker-list.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { ActionSheetController } from '@ionic/angular'; 4 | import { ConferenceData } from '../../providers/conference-data'; 5 | 6 | @Component({ 7 | selector: 'page-speaker-list', 8 | templateUrl: 'speaker-list.html', 9 | styleUrls: ['./speaker-list.scss'], 10 | }) 11 | export class SpeakerListPage { 12 | speakers: any[] = []; 13 | 14 | constructor( 15 | public actionSheetCtrl: ActionSheetController, 16 | public confData: ConferenceData, 17 | public router: Router 18 | ) {} 19 | 20 | ionViewDidEnter() { 21 | this.confData.getSpeakers().subscribe((speakers: any[]) => { 22 | this.speakers = speakers; 23 | }); 24 | } 25 | 26 | goToSpeakerTwitter(speaker: any) { 27 | } 28 | 29 | async openSpeakerShare(speaker: any) { 30 | const actionSheet = await this.actionSheetCtrl.create({ 31 | header: 'Share ' + speaker.name, 32 | buttons: [ 33 | { 34 | text: 'Copy Link', 35 | handler: () => { 36 | navigator.clipboard.writeText(window.location.origin + `/app/tabs/speakers/speaker-details/${speaker.id}`); 37 | } 38 | }, 39 | { 40 | text: 'Cancel', 41 | role: 'cancel' 42 | } 43 | ] 44 | }); 45 | 46 | await actionSheet.present(); 47 | } 48 | 49 | async openContact(speaker: any) { 50 | const mode = 'ios'; // this.config.get('mode'); 51 | 52 | const actionSheet = await this.actionSheetCtrl.create({ 53 | header: 'Contact ' + speaker.name, 54 | buttons: [ 55 | { 56 | text: `Email ( ${speaker.email} )`, 57 | icon: mode !== 'ios' ? 'mail' : null, 58 | handler: () => { 59 | window.open('mailto:' + speaker.email); 60 | } 61 | }, 62 | { 63 | text: `Call ( ${speaker.phone} )`, 64 | icon: mode !== 'ios' ? 'call' : null, 65 | handler: () => { 66 | window.open('tel:' + speaker.phone); 67 | } 68 | } 69 | ] 70 | }); 71 | 72 | await actionSheet.present(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/pages/support/support-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SupportPage } from './support'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: SupportPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class SupportPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/support/support.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Support 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
17 | 18 | 19 | Enter your support message below 20 | 21 | 22 | 23 | 24 | 25 |

26 | Support message is required 27 |

28 |
29 | 30 |
31 | Submit 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/app/pages/support/support.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { IonicModule } from '@ionic/angular'; 5 | import { SupportPage } from './support'; 6 | import { SupportPageRoutingModule } from './support-routing.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | FormsModule, 12 | IonicModule, 13 | SupportPageRoutingModule 14 | ], 15 | declarations: [ 16 | SupportPage, 17 | ] 18 | }) 19 | export class SupportModule { } 20 | -------------------------------------------------------------------------------- /src/app/pages/support/support.scss: -------------------------------------------------------------------------------- 1 | .support-logo { 2 | padding: 20px 0; 3 | min-height: 200px; 4 | text-align: center; 5 | } 6 | 7 | .support-logo img { 8 | max-width: 150px; 9 | } 10 | 11 | .list { 12 | margin-bottom: 0; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/support/support.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { AlertController, ToastController } from '@ionic/angular'; 4 | 5 | @Component({ 6 | selector: 'page-support', 7 | templateUrl: 'support.html', 8 | styleUrls: ['./support.scss'], 9 | }) 10 | export class SupportPage { 11 | submitted = false; 12 | supportMessage: string; 13 | 14 | constructor( 15 | public alertCtrl: AlertController, 16 | public toastCtrl: ToastController 17 | ) { } 18 | 19 | async ionViewDidEnter() { 20 | const toast = await this.toastCtrl.create({ 21 | message: 'This does not actually send a support request.', 22 | duration: 3000 23 | }); 24 | await toast.present(); 25 | } 26 | 27 | async submit(form: NgForm) { 28 | this.submitted = true; 29 | 30 | if (form.valid) { 31 | this.supportMessage = ''; 32 | this.submitted = false; 33 | 34 | const toast = await this.toastCtrl.create({ 35 | message: 'Your support request has been sent.', 36 | duration: 3000 37 | }); 38 | await toast.present(); 39 | } 40 | } 41 | 42 | // If the user enters text in the support question and then navigates 43 | // without submitting first, ask if they meant to leave the page 44 | // async ionViewCanLeave(): Promise { 45 | // // If the support message is empty we should just navigate 46 | // if (!this.supportMessage || this.supportMessage.trim().length === 0) { 47 | // return true; 48 | // } 49 | 50 | // return new Promise((resolve: any, reject: any) => { 51 | // const alert = await this.alertCtrl.create({ 52 | // title: 'Leave this page?', 53 | // message: 'Are you sure you want to leave this page? Your support message will not be submitted.', 54 | // buttons: [ 55 | // { text: 'Stay', handler: reject }, 56 | // { text: 'Leave', role: 'cancel', handler: resolve } 57 | // ] 58 | // }); 59 | 60 | // await alert.present(); 61 | // }); 62 | // } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/pages/tabs-page/tabs-page-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { TabsPage } from './tabs-page'; 4 | import { SchedulePage } from '../schedule/schedule'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'tabs', 9 | component: TabsPage, 10 | children: [ 11 | { 12 | path: 'schedule', 13 | children: [ 14 | { 15 | path: '', 16 | component: SchedulePage, 17 | }, 18 | { 19 | path: 'session/:sessionId', 20 | loadChildren: () => import('../session-detail/session-detail.module').then(m => m.SessionDetailModule) 21 | } 22 | ] 23 | }, 24 | { 25 | path: 'speakers', 26 | children: [ 27 | { 28 | path: '', 29 | loadChildren: () => import('../speaker-list/speaker-list.module').then(m => m.SpeakerListModule) 30 | }, 31 | { 32 | path: 'session/:sessionId', 33 | loadChildren: () => import('../session-detail/session-detail.module').then(m => m.SessionDetailModule) 34 | }, 35 | { 36 | path: 'speaker-details/:speakerId', 37 | loadChildren: () => import('../speaker-detail/speaker-detail.module').then(m => m.SpeakerDetailModule) 38 | } 39 | ] 40 | }, 41 | { 42 | path: 'map', 43 | children: [ 44 | { 45 | path: '', 46 | loadChildren: () => import('../map/map.module').then(m => m.MapModule) 47 | } 48 | ] 49 | }, 50 | { 51 | path: 'about', 52 | children: [ 53 | { 54 | path: '', 55 | loadChildren: () => import('../about/about.module').then(m => m.AboutModule) 56 | } 57 | ] 58 | }, 59 | { 60 | path: '', 61 | redirectTo: '/app/tabs/schedule', 62 | pathMatch: 'full' 63 | } 64 | ] 65 | } 66 | ]; 67 | 68 | @NgModule({ 69 | imports: [RouterModule.forChild(routes)], 70 | exports: [RouterModule] 71 | }) 72 | export class TabsPageRoutingModule { } 73 | 74 | -------------------------------------------------------------------------------- /src/app/pages/tabs-page/tabs-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Schedule 7 | 8 | 9 | 10 | 11 | Speakers 12 | 13 | 14 | 15 | 16 | Map 17 | 18 | 19 | 20 | 21 | About 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/app/pages/tabs-page/tabs-page.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { TabsPage } from './tabs-page'; 5 | import { TabsPageRoutingModule } from './tabs-page-routing.module'; 6 | import { AboutModule } from '../about/about.module'; 7 | import { MapModule } from '../map/map.module'; 8 | import { ScheduleModule } from '../schedule/schedule.module'; 9 | import { SessionDetailModule } from '../session-detail/session-detail.module'; 10 | import { SpeakerDetailModule } from '../speaker-detail/speaker-detail.module'; 11 | import { SpeakerListModule } from '../speaker-list/speaker-list.module'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | AboutModule, 16 | CommonModule, 17 | IonicModule, 18 | MapModule, 19 | ScheduleModule, 20 | SessionDetailModule, 21 | SpeakerDetailModule, 22 | SpeakerListModule, 23 | TabsPageRoutingModule 24 | ], 25 | declarations: [ 26 | TabsPage, 27 | ] 28 | }) 29 | export class TabsModule { } 30 | -------------------------------------------------------------------------------- /src/app/pages/tabs-page/tabs-page.scss: -------------------------------------------------------------------------------- 1 | .tabbar { 2 | justify-content: center; 3 | } 4 | 5 | .tab-button { 6 | max-width: 200px; 7 | } -------------------------------------------------------------------------------- /src/app/pages/tabs-page/tabs-page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: 'tabs-page.html' 5 | }) 6 | export class TabsPage {} 7 | -------------------------------------------------------------------------------- /src/app/pages/tutorial/tutorial-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { TutorialPage } from './tutorial'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: TutorialPage 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class TutorialPageRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/pages/tutorial/tutorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Skip 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | Welcome to 15 | ICA 16 |

17 |

18 | The 19 | ionic conference app is a practical preview of the ionic framework in action, and a demonstration of proper code 20 | use. 21 |

22 |
23 | 24 | 25 | 26 |

What is Ionic?

27 |

28 | Ionic Framework is an open source SDK that enables developers to build high quality mobile apps with web technologies 29 | like HTML, CSS, and JavaScript.

30 |
31 | 32 | 33 | 34 |

What is Ionic Appflow?

35 |

36 | Ionic Appflow is a powerful set of services and features built on top of Ionic Framework that brings a totally new 37 | level of app development agility to mobile dev teams.

38 |
39 | 40 | 41 | 42 |

Ready to Play?

43 | 44 | Continue 45 | 46 | 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /src/app/pages/tutorial/tutorial.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { TutorialPage } from './tutorial'; 5 | import { TutorialPageRoutingModule } from './tutorial-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | IonicModule, 11 | TutorialPageRoutingModule 12 | ], 13 | declarations: [TutorialPage], 14 | entryComponents: [TutorialPage], 15 | }) 16 | export class TutorialModule {} 17 | -------------------------------------------------------------------------------- /src/app/pages/tutorial/tutorial.scss: -------------------------------------------------------------------------------- 1 | ion-toolbar { 2 | // TODO test transparent and fullscreen 3 | --background: transparent; 4 | --border-color: transparent; 5 | } 6 | 7 | .swiper-slide { 8 | display: block; 9 | } 10 | 11 | .slide-title { 12 | margin-top: 2.8rem; 13 | } 14 | 15 | .slide-image { 16 | max-height: 50%; 17 | max-width: 60%; 18 | margin: 36px 0; 19 | pointer-events: none; 20 | } 21 | 22 | b { 23 | font-weight: 500; 24 | } 25 | 26 | p { 27 | padding: 0 40px; 28 | font-size: 14px; 29 | line-height: 1.5; 30 | color: var(--ion-color-step-600, #60646b); 31 | 32 | b { 33 | color: var(--ion-text-color, #000000); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/pages/tutorial/tutorial.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { MenuController, IonSlides } from '@ionic/angular'; 4 | import { Storage } from '@ionic/storage'; 5 | 6 | @Component({ 7 | selector: 'page-tutorial', 8 | templateUrl: 'tutorial.html', 9 | styleUrls: ['./tutorial.scss'], 10 | }) 11 | export class TutorialPage { 12 | showSkip = true; 13 | 14 | @ViewChild('slides', { static: true }) slides: IonSlides; 15 | 16 | constructor( 17 | public menu: MenuController, 18 | public router: Router, 19 | public storage: Storage 20 | ) {} 21 | 22 | startApp() { 23 | this.router 24 | .navigateByUrl('/app/tabs/schedule', { replaceUrl: true }) 25 | .then(() => this.storage.set('ion_did_tutorial', true)); 26 | } 27 | 28 | onSlideChangeStart(event) { 29 | event.target.isEnd().then(isEnd => { 30 | this.showSkip = !isEnd; 31 | }); 32 | } 33 | 34 | ionViewWillEnter() { 35 | this.storage.get('ion_did_tutorial').then(res => { 36 | if (res === true) { 37 | this.router.navigateByUrl('/app/tabs/schedule', { replaceUrl: true }); 38 | } 39 | }); 40 | 41 | this.menu.enable(false); 42 | } 43 | 44 | ionViewDidLeave() { 45 | // enable the root left menu when leaving the tutorial page 46 | this.menu.enable(true); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/providers/check-tutorial.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanLoad, Router } from '@angular/router'; 3 | import { Storage } from '@ionic/storage'; 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class CheckTutorial implements CanLoad { 8 | constructor(private storage: Storage, private router: Router) {} 9 | 10 | canLoad() { 11 | return this.storage.get('ion_did_tutorial').then(res => { 12 | if (res) { 13 | this.router.navigate(['/app', 'tabs', 'schedule']); 14 | return false; 15 | } else { 16 | return true; 17 | } 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/providers/conference-data.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { of } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { UserData } from './user-data'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class ConferenceData { 11 | data: any; 12 | 13 | constructor(public http: HttpClient, public user: UserData) {} 14 | 15 | load(): any { 16 | if (this.data) { 17 | return of(this.data); 18 | } else { 19 | return this.http 20 | .get('assets/data/data.json') 21 | .pipe(map(this.processData, this)); 22 | } 23 | } 24 | 25 | processData(data: any) { 26 | // just some good 'ol JS fun with objects and arrays 27 | // build up the data by linking speakers to sessions 28 | this.data = data; 29 | 30 | // loop through each day in the schedule 31 | this.data.schedule.forEach((day: any) => { 32 | // loop through each timeline group in the day 33 | day.groups.forEach((group: any) => { 34 | // loop through each session in the timeline group 35 | group.sessions.forEach((session: any) => { 36 | session.speakers = []; 37 | if (session.speakerNames) { 38 | session.speakerNames.forEach((speakerName: any) => { 39 | const speaker = this.data.speakers.find( 40 | (s: any) => s.name === speakerName 41 | ); 42 | if (speaker) { 43 | session.speakers.push(speaker); 44 | speaker.sessions = speaker.sessions || []; 45 | speaker.sessions.push(session); 46 | } 47 | }); 48 | } 49 | }); 50 | }); 51 | }); 52 | 53 | return this.data; 54 | } 55 | 56 | getTimeline( 57 | dayIndex: number, 58 | queryText = '', 59 | excludeTracks: any[] = [], 60 | segment = 'all' 61 | ) { 62 | return this.load().pipe( 63 | map((data: any) => { 64 | const day = data.schedule[dayIndex]; 65 | day.shownSessions = 0; 66 | 67 | queryText = queryText.toLowerCase().replace(/,|\.|-/g, ' '); 68 | const queryWords = queryText.split(' ').filter(w => !!w.trim().length); 69 | 70 | day.groups.forEach((group: any) => { 71 | group.hide = true; 72 | 73 | group.sessions.forEach((session: any) => { 74 | // check if this session should show or not 75 | this.filterSession(session, queryWords, excludeTracks, segment); 76 | 77 | if (!session.hide) { 78 | // if this session is not hidden then this group should show 79 | group.hide = false; 80 | day.shownSessions++; 81 | } 82 | }); 83 | }); 84 | 85 | return day; 86 | }) 87 | ); 88 | } 89 | 90 | filterSession( 91 | session: any, 92 | queryWords: string[], 93 | excludeTracks: any[], 94 | segment: string 95 | ) { 96 | let matchesQueryText = false; 97 | if (queryWords.length) { 98 | // of any query word is in the session name than it passes the query test 99 | queryWords.forEach((queryWord: string) => { 100 | if (session.name.toLowerCase().indexOf(queryWord) > -1) { 101 | matchesQueryText = true; 102 | } 103 | }); 104 | } else { 105 | // if there are no query words then this session passes the query test 106 | matchesQueryText = true; 107 | } 108 | 109 | // if any of the sessions tracks are not in the 110 | // exclude tracks then this session passes the track test 111 | let matchesTracks = false; 112 | session.tracks.forEach((trackName: string) => { 113 | if (excludeTracks.indexOf(trackName) === -1) { 114 | matchesTracks = true; 115 | } 116 | }); 117 | 118 | // if the segment is 'favorites', but session is not a user favorite 119 | // then this session does not pass the segment test 120 | let matchesSegment = false; 121 | if (segment === 'favorites') { 122 | if (this.user.hasFavorite(session.name)) { 123 | matchesSegment = true; 124 | } 125 | } else { 126 | matchesSegment = true; 127 | } 128 | 129 | // all tests must be true if it should not be hidden 130 | session.hide = !(matchesQueryText && matchesTracks && matchesSegment); 131 | } 132 | 133 | getSpeakers() { 134 | return this.load().pipe( 135 | map((data: any) => { 136 | return data.speakers.sort((a: any, b: any) => { 137 | const aName = a.name.split(' ').pop(); 138 | const bName = b.name.split(' ').pop(); 139 | return aName.localeCompare(bName); 140 | }); 141 | }) 142 | ); 143 | } 144 | 145 | getTracks() { 146 | return this.load().pipe( 147 | map((data: any) => { 148 | return data.tracks.sort(); 149 | }) 150 | ); 151 | } 152 | 153 | getMap() { 154 | return this.load().pipe( 155 | map((data: any) => { 156 | return data.map; 157 | }) 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/app/providers/user-data.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Storage } from '@ionic/storage'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class UserData { 8 | _favorites: string[] = []; 9 | HAS_LOGGED_IN = 'hasLoggedIn'; 10 | HAS_SEEN_TUTORIAL = 'hasSeenTutorial'; 11 | 12 | constructor( 13 | public storage: Storage 14 | ) { } 15 | 16 | hasFavorite(sessionName: string): boolean { 17 | return (this._favorites.indexOf(sessionName) > -1); 18 | } 19 | 20 | addFavorite(sessionName: string): void { 21 | this._favorites.push(sessionName); 22 | } 23 | 24 | removeFavorite(sessionName: string): void { 25 | const index = this._favorites.indexOf(sessionName); 26 | if (index > -1) { 27 | this._favorites.splice(index, 1); 28 | } 29 | } 30 | 31 | login(username: string): Promise { 32 | return this.storage.set(this.HAS_LOGGED_IN, true).then(() => { 33 | this.setUsername(username); 34 | return window.dispatchEvent(new CustomEvent('user:login')); 35 | }); 36 | } 37 | 38 | signup(username: string): Promise { 39 | return this.storage.set(this.HAS_LOGGED_IN, true).then(() => { 40 | this.setUsername(username); 41 | return window.dispatchEvent(new CustomEvent('user:signup')); 42 | }); 43 | } 44 | 45 | logout(): Promise { 46 | return this.storage.remove(this.HAS_LOGGED_IN).then(() => { 47 | return this.storage.remove('username'); 48 | }).then(() => { 49 | window.dispatchEvent(new CustomEvent('user:logout')); 50 | }); 51 | } 52 | 53 | setUsername(username: string): Promise { 54 | return this.storage.set('username', username); 55 | } 56 | 57 | getUsername(): Promise { 58 | return this.storage.get('username').then((value) => { 59 | return value; 60 | }); 61 | } 62 | 63 | isLoggedIn(): Promise { 64 | return this.storage.get(this.HAS_LOGGED_IN).then((value) => { 65 | return value === true; 66 | }); 67 | } 68 | 69 | checkHasSeenTutorial(): Promise { 70 | return this.storage.get(this.HAS_SEEN_TUTORIAL).then((value) => { 71 | return value; 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/assets/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "schedule": [ 3 | { 4 | "date": "2047-05-17", 5 | "groups": [ 6 | { 7 | "time": "8:00 am", 8 | "sessions": [ 9 | { 10 | "name": "Breakfast", 11 | "timeStart": "8:00 am", 12 | "timeEnd": "9:00 am", 13 | "location": "Dining Hall", 14 | "tracks": ["Food"], 15 | "id": "1" 16 | } 17 | ] 18 | }, 19 | { 20 | "time": "9:15 am", 21 | "sessions": [ 22 | { 23 | "name": "Getting Started with Ionic", 24 | "location": "Hall 2", 25 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 26 | "speakerNames": ["Ted Turtle"], 27 | "timeStart": "9:30 am", 28 | "timeEnd": "9:45 am", 29 | "tracks": ["Ionic"], 30 | "id": "2" 31 | }, 32 | { 33 | "name": "Ionic Tooling", 34 | "location": "Executive Ballroom", 35 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 36 | "speakerNames": ["Rachel Rabbit"], 37 | "timeStart": "9:45 am", 38 | "timeEnd": "10:00 am", 39 | "tracks": ["Tooling"], 40 | "id": "3" 41 | }, 42 | { 43 | "name": "University of Ionic", 44 | "location": "Hall 3", 45 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 46 | "speakerNames": ["Ellie Elephant"], 47 | "timeStart": "9:15 am", 48 | "timeEnd": "9:30 am", 49 | "tracks": ["Ionic"], 50 | "id": "4" 51 | } 52 | ] 53 | }, 54 | { 55 | "time": "10:00 am", 56 | "sessions": [ 57 | { 58 | "name": "Migrating to Ionic", 59 | "location": "Hall 1", 60 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 61 | "speakerNames": ["Eva Eagle", "Lionel Lion"], 62 | "timeStart": "10:00 am", 63 | "timeEnd": "10:15 am", 64 | "tracks": ["Ionic"], 65 | "id": "5" 66 | }, 67 | { 68 | "name": "What's New in Angular", 69 | "location": "Hall 3", 70 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 71 | "speakerNames": ["Rachel Rabbit"], 72 | "timeStart": "10:15 am", 73 | "timeEnd": "10:30 am", 74 | "tracks": ["Angular"], 75 | "id": "6" 76 | }, 77 | { 78 | "name": "The Evolution of Ionicons", 79 | "location": "Hall 2", 80 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 81 | "speakerNames": ["Isabella Iguana", "Eva Eagle"], 82 | "timeStart": "10:15 am", 83 | "timeEnd": "10:30 am", 84 | "tracks": ["Design"], 85 | "id": "7" 86 | }, 87 | { 88 | "name": "Ionic Pro", 89 | "location": "Grand Ballroom A", 90 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 91 | "speakerNames": ["Charlie Cheetah"], 92 | "timeStart": "10:45 am", 93 | "timeEnd": "11:00 am", 94 | "tracks": ["Services"], 95 | "id": "8" 96 | } 97 | ] 98 | }, 99 | { 100 | "time": "11:00 am", 101 | "sessions": [ 102 | { 103 | "name": "Ionic Workshop", 104 | "location": "Hall 1", 105 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 106 | "speakerNames": ["Karl Kitten", "Lionel Lion"], 107 | "timeStart": "11:00 am", 108 | "timeEnd": "11:45 am", 109 | "tracks": ["Workshop"], 110 | "id": "9" 111 | }, 112 | { 113 | "name": "Community Interaction", 114 | "location": "Hall 3", 115 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 116 | "speakerNames": ["Lionel Lion", "Gino Giraffe"], 117 | "timeStart": "11:30 am", 118 | "timeEnd": "11:50 am", 119 | "tracks": ["Communication"], 120 | "id": "10" 121 | }, 122 | { 123 | "name": "Navigation in Ionic", 124 | "location": "Grand Ballroom A", 125 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 126 | "speakerNames": ["Rachel Rabbit", "Eva Eagle"], 127 | "timeStart": "11:30 am", 128 | "timeEnd": "12:00 pm", 129 | "tracks": ["Navigation"], 130 | "id": "11" 131 | } 132 | ] 133 | }, 134 | { 135 | "time": "12:00 pm", 136 | "sessions": [ 137 | { 138 | "name": "Lunch", 139 | "location": "Dining Hall", 140 | "description": "Come grab lunch with all the Ionic fanatics and talk all things Ionic", 141 | "timeStart": "12:00 pm", 142 | "timeEnd": "1:00 pm", 143 | "tracks": ["Food"], 144 | "id": "12" 145 | } 146 | ] 147 | }, 148 | { 149 | "time": "1:00 pm", 150 | "sessions": [ 151 | { 152 | "name": "Ionic in the Enterprise", 153 | "location": "Hall 1", 154 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 155 | "speakerNames": ["Paul Puppy"], 156 | "timeStart": "1:00 pm", 157 | "timeEnd": "1:15 pm", 158 | "tracks": ["Communication"], 159 | "id": "13" 160 | }, 161 | { 162 | "name": "Ionic Worldwide", 163 | "location": "Hall 1", 164 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 165 | "speakerNames": ["Gino Giraffe"], 166 | "timeStart": "1:15 pm", 167 | "timeEnd": "1:30 pm", 168 | "tracks": ["Communication"], 169 | "id": "14" 170 | }, 171 | { 172 | "name": "The Ionic Package", 173 | "location": "Grand Ballroom B", 174 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 175 | "speakerNames": ["Molly Mouse", "Burt Bear"], 176 | "timeStart": "1:30 pm", 177 | "timeEnd": "2:00 pm", 178 | "tracks": ["Services"], 179 | "id": "15" 180 | } 181 | ] 182 | }, 183 | { 184 | "time": "2:00 pm", 185 | "sessions": [ 186 | { 187 | "name": "Push Notifications in Ionic", 188 | "location": "Hall 2", 189 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 190 | "speakerNames": ["Burt Bear", "Charlie Cheetah"], 191 | "timeStart": "2:00 pm", 192 | "timeEnd": "2:30 pm", 193 | "tracks": ["Services"], 194 | "id": "16" 195 | }, 196 | { 197 | "name": "Ionic Documentation", 198 | "location": "Grand Ballroom B", 199 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 200 | "speakerNames": ["Donald Duck"], 201 | "timeStart": "2:30 pm", 202 | "timeEnd": "2:45 pm", 203 | "tracks": ["Documentation"], 204 | "id": "17" 205 | }, 206 | { 207 | "name": "UX in Ionic", 208 | "location": "Hall 3", 209 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 210 | "speakerNames": ["Isabella Iguana", "Ellie Elephant"], 211 | "timeStart": "2:45 pm", 212 | "timeEnd": "3:00 pm", 213 | "tracks": ["Design"], 214 | "id": "18" 215 | } 216 | ] 217 | }, 218 | { 219 | "time": "3:00", 220 | "sessions": [ 221 | { 222 | "name": "Angular Directives in Ionic", 223 | "location": "Hall 1", 224 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 225 | "speakerNames": ["Ted Turtle"], 226 | "timeStart": "3:00 pm", 227 | "timeEnd": "3:30 pm", 228 | "tracks": ["Angular"], 229 | "id": "19" 230 | }, 231 | { 232 | "name": "Mobile States", 233 | "location": "Hall 2", 234 | "description": "Mobile devices and browsers are now advanced enough that developers can build native-quality mobile apps using open web technologies like HTML5, Javascript, and CSS. In this talk, we’ll provide background on why and how we created Ionic, the design decisions made as we integrated Ionic with Angular, and the performance considerations for mobile platforms that our team had to overcome. We’ll also review new and upcoming Ionic features, and talk about the hidden powers and benefits of combining mobile app development and Angular.", 235 | "speakerNames": ["Rachel Rabbit"], 236 | "timeStart": "3:30 pm", 237 | "timeEnd": "3:45 pm", 238 | "tracks": ["Navigation"], 239 | "id": "20" 240 | } 241 | ] 242 | } 243 | ] 244 | } 245 | ], 246 | 247 | "speakers": [ 248 | { 249 | "name": "Burt Bear", 250 | "profilePic": "assets/img/speakers/bear.jpg", 251 | "instagram": "ionicframework", 252 | "twitter": "ionicframework", 253 | "github": "ionic-team/ionic", 254 | "about": "Burt is a Bear.", 255 | "location": "Everywhere", 256 | "email": "burt@example.com", 257 | "phone": "+1-541-754-3010", 258 | "id": "1" 259 | }, 260 | { 261 | "name": "Charlie Cheetah", 262 | "profilePic": "assets/img/speakers/cheetah.jpg", 263 | "instagram": "ionicframework", 264 | "twitter": "ionicframework", 265 | "github": "ionic-team/ionic", 266 | "about": "Charlie is a Cheetah.", 267 | "location": "Everywhere", 268 | "email": "charlie@example.com", 269 | "phone": "+1-541-754-3010", 270 | "id": "2" 271 | }, 272 | { 273 | "name": "Donald Duck", 274 | "profilePic": "assets/img/speakers/duck.jpg", 275 | "instagram": "ionicframework", 276 | "twitter": "ionicframework", 277 | "github": "ionic-team/ionic", 278 | "about": "Donald is a Duck.", 279 | "location": "Everywhere", 280 | "email": "donald@example.com", 281 | "phone": "+1-541-754-3010", 282 | "id": "3" 283 | }, 284 | { 285 | "name": "Eva Eagle", 286 | "profilePic": "assets/img/speakers/eagle.jpg", 287 | "instagram": "ionicframework", 288 | "twitter": "ionicframework", 289 | "github": "ionic-team/ionic", 290 | "about": "Eva is an Eagle.", 291 | "location": "Everywhere", 292 | "email": "eva@example.com", 293 | "phone": "+1-541-754-3010", 294 | "id": "4" 295 | }, 296 | { 297 | "name": "Ellie Elephant", 298 | "profilePic": "assets/img/speakers/elephant.jpg", 299 | "instagram": "ionicframework", 300 | "twitter": "ionicframework", 301 | "github": "ionic-team/ionic", 302 | "about": "Ellie is an Elephant.", 303 | "location": "Everywhere", 304 | "email": "ellie@example.com", 305 | "phone": "+1-541-754-3010", 306 | "id": "5" 307 | }, 308 | { 309 | "name": "Gino Giraffe", 310 | "profilePic": "assets/img/speakers/giraffe.jpg", 311 | "instagram": "ionicframework", 312 | "twitter": "ionicframework", 313 | "github": "ionic-team/ionic", 314 | "about": "Gino is a Giraffe.", 315 | "location": "Everywhere", 316 | "email": "gino@example.com", 317 | "phone": "+1-541-754-3010", 318 | "id": "6" 319 | }, 320 | { 321 | "name": "Isabella Iguana", 322 | "profilePic": "assets/img/speakers/iguana.jpg", 323 | "instagram": "ionicframework", 324 | "twitter": "ionicframework", 325 | "github": "ionic-team/ionic", 326 | "about": "Isabella is an Iguana.", 327 | "location": "Everywhere", 328 | "email": "isabella@example.com", 329 | "phone": "+1-541-754-3010", 330 | "id": "7" 331 | }, 332 | { 333 | "name": "Karl Kitten", 334 | "profilePic": "assets/img/speakers/kitten.jpg", 335 | "instagram": "ionicframework", 336 | "twitter": "ionicframework", 337 | "github": "ionic-team/ionic", 338 | "about": "Karl is a Kitten.", 339 | "location": "Everywhere", 340 | "email": "karl@example.com", 341 | "phone": "+1-541-754-3010", 342 | "id": "8" 343 | }, 344 | { 345 | "name": "Lionel Lion", 346 | "profilePic": "assets/img/speakers/lion.jpg", 347 | "instagram": "ionicframework", 348 | "twitter": "ionicframework", 349 | "github": "ionic-team/ionic", 350 | "about": "Lionel is a Lion.", 351 | "location": "Everywhere", 352 | "email": "lionel@example.com", 353 | "phone": "+1-541-754-3010", 354 | "id": "9" 355 | }, 356 | { 357 | "name": "Molly Mouse", 358 | "profilePic": "assets/img/speakers/mouse.jpg", 359 | "instagram": "ionicframework", 360 | "twitter": "ionicframework", 361 | "github": "ionic-team/ionic", 362 | "about": "Molly is a Mouse.", 363 | "location": "Everywhere", 364 | "email": "molly@example.com", 365 | "phone": "+1-541-754-3010", 366 | "id": "10" 367 | }, 368 | { 369 | "name": "Paul Puppy", 370 | "profilePic": "assets/img/speakers/puppy.jpg", 371 | "instagram": "ionicframework", 372 | "twitter": "ionicframework", 373 | "github": "ionic-team/ionic", 374 | "about": "Paul is a Puppy.", 375 | "location": "Everywhere", 376 | "email": "paul@example.com", 377 | "phone": "+1-541-754-3010", 378 | "id": "11" 379 | }, 380 | { 381 | "name": "Rachel Rabbit", 382 | "profilePic": "assets/img/speakers/rabbit.jpg", 383 | "instagram": "ionicframework", 384 | "twitter": "ionicframework", 385 | "github": "ionic-team/ionic", 386 | "about": "Rachel is a Rabbit.", 387 | "location": "Everywhere", 388 | "email": "rachel@example.com", 389 | "phone": "+1-541-754-3010", 390 | "id": "12" 391 | }, 392 | { 393 | "name": "Ted Turtle", 394 | "profilePic": "assets/img/speakers/turtle.jpg", 395 | "instagram": "ionicframework", 396 | "twitter": "ionicframework", 397 | "github": "ionic-team/ionic", 398 | "about": "Ted is a Turtle.", 399 | "location": "Everywhere", 400 | "email": "ted@example.com", 401 | "phone": "+1-541-754-3010", 402 | "id": "13" 403 | } 404 | ], 405 | 406 | "map": [ 407 | { 408 | "name": "Monona Terrace Convention Center", 409 | "lat": 43.071584, 410 | "lng": -89.38012, 411 | "center": true 412 | }, 413 | { 414 | "name": "Ionic HQ", 415 | "lat": 43.074395, 416 | "lng": -89.381056 417 | }, 418 | { 419 | "name": "Afterparty - Brocach Irish Pub", 420 | "lat": 43.07336, 421 | "lng": -89.38335 422 | } 423 | ], 424 | 425 | "tracks": [ 426 | { 427 | "name": "Angular", 428 | "icon": "logo-angular" 429 | }, 430 | { 431 | "name": "Documentation", 432 | "icon": "document" 433 | }, 434 | { 435 | "name": "Food", 436 | "icon": "restaurant" 437 | }, 438 | { 439 | "name": "Ionic", 440 | "icon": "logo-ionic" 441 | }, 442 | { 443 | "name": "Tooling", 444 | "icon": "hammer" 445 | }, 446 | { 447 | "name": "Design", 448 | "icon": "color-palette" 449 | }, 450 | { 451 | "name": "Services", 452 | "icon": "cog" 453 | }, 454 | { 455 | "name": "Workshop", 456 | "icon": "construct" 457 | }, 458 | { 459 | "name": "Communication", 460 | "icon": "call" 461 | }, 462 | { 463 | "name": "Navigation", 464 | "icon": "compass" 465 | } 466 | ] 467 | } 468 | -------------------------------------------------------------------------------- /src/assets/icons/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/icons/.gitkeep -------------------------------------------------------------------------------- /src/assets/img/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/appicon.png -------------------------------------------------------------------------------- /src/assets/img/appicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/img/ica-slidebox-img-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/ica-slidebox-img-1.png -------------------------------------------------------------------------------- /src/assets/img/ica-slidebox-img-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/ica-slidebox-img-2.png -------------------------------------------------------------------------------- /src/assets/img/ica-slidebox-img-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/ica-slidebox-img-3.png -------------------------------------------------------------------------------- /src/assets/img/ica-slidebox-img-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/ica-slidebox-img-4.png -------------------------------------------------------------------------------- /src/assets/img/ionic-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/img/speakers/bear.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/bear.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/cheetah.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/cheetah.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/duck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/duck.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/eagle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/eagle.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/elephant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/elephant.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/giraffe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/giraffe.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/iguana.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/iguana.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/kitten.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/lion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/lion.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/mouse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/mouse.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/puppy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/puppy.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/rabbit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/rabbit.jpg -------------------------------------------------------------------------------- /src/assets/img/speakers/turtle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elegantapp/pwa-workshop-angular-firebase/b98d3d9d591d15703c9a012175832ed26d2be540/src/assets/img/speakers/turtle.jpg -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * App Global CSS 3 | * ---------------------------------------------------------------------------- 4 | * Put style rules here that you want to apply globally. These styles are for 5 | * the entire app and not just one component. Additionally, this file can be 6 | * used as an entry point to import other CSS/Sass files to be included in the 7 | * output CSS. 8 | * For more information on global stylesheets, visit the documentation: 9 | * https://ionicframework.com/docs/layout/global-stylesheets 10 | */ 11 | 12 | /* Core CSS required for Ionic components to work properly */ 13 | @import "~@ionic/angular/css/core.css"; 14 | 15 | /* Basic CSS for apps built with Ionic */ 16 | @import "~@ionic/angular/css/normalize.css"; 17 | @import "~@ionic/angular/css/structure.css"; 18 | @import "~@ionic/angular/css/typography.css"; 19 | 20 | /* Optional CSS utils that can be commented out */ 21 | @import "~@ionic/angular/css/padding.css"; 22 | @import "~@ionic/angular/css/float-elements.css"; 23 | @import "~@ionic/angular/css/text-alignment.css"; 24 | @import "~@ionic/angular/css/text-transformation.css"; 25 | @import "~@ionic/angular/css/flex-utils.css"; 26 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic Conference PWA 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | import './zone-flags'; 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | 61 | import 'zone.js/dist/zone'; // Included with Angular CLI. 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Ionic Variables and Theming 3 | * ---------------------------------------------------------------------------- 4 | * For more information, please see 5 | * https://www.ionicframework.com/docs/theming/ 6 | */ 7 | 8 | /* 9 | * Ionic Colors 10 | * ---------------------------------------------------------------------------- 11 | * Named colors make it easy to reuse colors on various components. 12 | * It's highly recommended to change the default colors 13 | * to match your app's branding. Ionic provides nine layered colors 14 | * that can be changed to theme an app. Additional colors can be 15 | * added as well (see below). For more information, please see 16 | * https://www.ionicframework.com/docs/theming/colors 17 | * 18 | * To easily create custom color palettes for your app’s UI, 19 | * check out our color generator: 20 | * https://www.ionicframework.com/docs/theming/color-generator 21 | */ 22 | 23 | :root { 24 | --ion-color-primary: #3880ff; 25 | --ion-color-primary-rgb: 56, 128, 255; 26 | --ion-color-primary-contrast: #ffffff; 27 | --ion-color-primary-contrast-rgb: 255, 255, 255; 28 | --ion-color-primary-shade: #3171e0; 29 | --ion-color-primary-tint: #4c8dff; 30 | 31 | --ion-color-secondary: #0cd1e8; 32 | --ion-color-secondary-rgb: 12, 209, 232; 33 | --ion-color-secondary-contrast: #ffffff; 34 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 35 | --ion-color-secondary-shade: #0bb8cc; 36 | --ion-color-secondary-tint: #24d6ea; 37 | 38 | --ion-color-tertiary: #7044ff; 39 | --ion-color-tertiary-rgb: 112, 68, 255; 40 | --ion-color-tertiary-contrast: #ffffff; 41 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 42 | --ion-color-tertiary-shade: #633ce0; 43 | --ion-color-tertiary-tint: #7e57ff; 44 | 45 | --ion-color-success: #10dc60; 46 | --ion-color-success-rgb: 16, 220, 96; 47 | --ion-color-success-contrast: #ffffff; 48 | --ion-color-success-contrast-rgb: 255, 255, 255; 49 | --ion-color-success-shade: #0ec254; 50 | --ion-color-success-tint: #28e070; 51 | 52 | --ion-color-warning: #ffce00; 53 | --ion-color-warning-rgb: 255, 206, 0; 54 | --ion-color-warning-contrast: #ffffff; 55 | --ion-color-warning-contrast-rgb: 255, 255, 255; 56 | --ion-color-warning-shade: #e0b500; 57 | --ion-color-warning-tint: #ffd31a; 58 | 59 | --ion-color-danger: #f04141; 60 | --ion-color-danger-rgb: 245, 61, 61; 61 | --ion-color-danger-contrast: #ffffff; 62 | --ion-color-danger-contrast-rgb: 255, 255, 255; 63 | --ion-color-danger-shade: #d33939; 64 | --ion-color-danger-tint: #f25454; 65 | 66 | --ion-color-dark: #222428; 67 | --ion-color-dark-rgb: 34, 34, 34; 68 | --ion-color-dark-contrast: #ffffff; 69 | --ion-color-dark-contrast-rgb: 255, 255, 255; 70 | --ion-color-dark-shade: #1e2023; 71 | --ion-color-dark-tint: #383a3e; 72 | 73 | --ion-color-medium: #989aa2; 74 | --ion-color-medium-rgb: 152, 154, 162; 75 | --ion-color-medium-contrast: #ffffff; 76 | --ion-color-medium-contrast-rgb: 255, 255, 255; 77 | --ion-color-medium-shade: #86888f; 78 | --ion-color-medium-tint: #a2a4ab; 79 | 80 | --ion-color-light: #f4f5f8; 81 | --ion-color-light-rgb: 244, 244, 244; 82 | --ion-color-light-contrast: #000000; 83 | --ion-color-light-contrast-rgb: 0, 0, 0; 84 | --ion-color-light-shade: #d7d8da; 85 | --ion-color-light-tint: #f5f6f9; 86 | 87 | // Default Theme Colors 88 | --ion-background-color: #ffffff; 89 | --ion-background-color-rgb: 255,255,255; 90 | 91 | --ion-text-color: #000000; 92 | --ion-text-color-rgb: 0,0,0; 93 | 94 | --ion-color-step-50: #f2f2f2; 95 | --ion-color-step-100: #e6e6e6; 96 | --ion-color-step-150: #d9d9d9; 97 | --ion-color-step-200: #cccccc; 98 | --ion-color-step-250: #bfbfbf; 99 | --ion-color-step-300: #b3b3b3; 100 | --ion-color-step-350: #a6a6a6; 101 | --ion-color-step-400: #999999; 102 | --ion-color-step-450: #8c8c8c; 103 | --ion-color-step-500: #808080; 104 | --ion-color-step-550: #737373; 105 | --ion-color-step-600: #666666; 106 | --ion-color-step-650: #595959; 107 | --ion-color-step-700: #4d4d4d; 108 | --ion-color-step-750: #404040; 109 | --ion-color-step-800: #333333; 110 | --ion-color-step-850: #262626; 111 | --ion-color-step-900: #191919; 112 | --ion-color-step-950: #0d0d0d; 113 | } 114 | 115 | /* 116 | * Additional Ionic Colors 117 | * ---------------------------------------------------------------------------- 118 | * In order to add colors to be used within Ionic components, 119 | * the color should be added as a class with the convention `.ion-color-{COLOR}` 120 | * where `{COLOR}` is the color to be used on the Ionic component. 121 | * For more information on adding new colors, please see 122 | * https://ionicframework.com/docs/theming/colors#adding-colors 123 | * 124 | * To generate the code for a new color, check out our new color creator: 125 | * https://ionicframework.com/docs/theming/colors#new-color-creator 126 | */ 127 | 128 | :root { 129 | --ion-color-favorite: #69bb7b; 130 | --ion-color-favorite-rgb: 105,187,123; 131 | --ion-color-favorite-contrast: #ffffff; 132 | --ion-color-favorite-contrast-rgb: 255,255,255; 133 | --ion-color-favorite-shade: #5ca56c; 134 | --ion-color-favorite-tint: #78c288; 135 | 136 | --ion-color-twitter: #1da1f4; 137 | --ion-color-twitter-rgb: 29,161,244; 138 | --ion-color-twitter-contrast: #ffffff; 139 | --ion-color-twitter-contrast-rgb: 255,255,255; 140 | --ion-color-twitter-shade: #1a8ed7; 141 | --ion-color-twitter-tint: #34aaf5; 142 | 143 | --ion-color-google: #dc4a38; 144 | --ion-color-google-rgb: 220,74,56; 145 | --ion-color-google-contrast: #ffffff; 146 | --ion-color-google-contrast-rgb: 255,255,255; 147 | --ion-color-google-shade: #c24131; 148 | --ion-color-google-tint: #e05c4c; 149 | 150 | --ion-color-vimeo: #23b6ea; 151 | --ion-color-vimeo-rgb: 35,182,234; 152 | --ion-color-vimeo-contrast: #ffffff; 153 | --ion-color-vimeo-contrast-rgb: 255,255,255; 154 | --ion-color-vimeo-shade: #1fa0ce; 155 | --ion-color-vimeo-tint: #39bdec; 156 | 157 | --ion-color-facebook: #3b5998; 158 | --ion-color-facebook-rgb: 59,89,152; 159 | --ion-color-facebook-contrast: #ffffff; 160 | --ion-color-facebook-contrast-rgb: 255,255,255; 161 | --ion-color-facebook-shade: #344e86; 162 | --ion-color-facebook-tint: #4f6aa2; 163 | } 164 | 165 | .ion-color-favorite { 166 | --ion-color-base: var(--ion-color-favorite); 167 | --ion-color-base-rgb: var(--ion-color-favorite-rgb); 168 | --ion-color-contrast: var(--ion-color-favorite-contrast); 169 | --ion-color-contrast-rgb: var(--ion-color-favorite-contrast-rgb); 170 | --ion-color-shade: var(--ion-color-favorite-shade); 171 | --ion-color-tint: var(--ion-color-favorite-tint); 172 | } 173 | 174 | .ion-color-twitter { 175 | --ion-color-base: var(--ion-color-twitter); 176 | --ion-color-base-rgb: var(--ion-color-twitter-rgb); 177 | --ion-color-contrast: var(--ion-color-twitter-contrast); 178 | --ion-color-contrast-rgb: var(--ion-color-twitter-contrast-rgb); 179 | --ion-color-shade: var(--ion-color-twitter-shade); 180 | --ion-color-tint: var(--ion-color-twitter-tint); 181 | } 182 | 183 | .ion-color-google { 184 | --ion-color-base: var(--ion-color-google); 185 | --ion-color-base-rgb: var(--ion-color-google-rgb); 186 | --ion-color-contrast: var(--ion-color-google-contrast); 187 | --ion-color-contrast-rgb: var(--ion-color-google-contrast-rgb); 188 | --ion-color-shade: var(--ion-color-google-shade); 189 | --ion-color-tint: var(--ion-color-google-tint); 190 | } 191 | 192 | .ion-color-vimeo { 193 | --ion-color-base: var(--ion-color-vimeo); 194 | --ion-color-base-rgb: var(--ion-color-vimeo-rgb); 195 | --ion-color-contrast: var(--ion-color-vimeo-contrast); 196 | --ion-color-contrast-rgb: var(--ion-color-vimeo-contrast-rgb); 197 | --ion-color-shade: var(--ion-color-vimeo-shade); 198 | --ion-color-tint: var(--ion-color-vimeo-tint); 199 | } 200 | 201 | .ion-color-facebook { 202 | --ion-color-base: var(--ion-color-facebook); 203 | --ion-color-base-rgb: var(--ion-color-facebook-rgb); 204 | --ion-color-contrast: var(--ion-color-facebook-contrast); 205 | --ion-color-contrast-rgb: var(--ion-color-facebook-contrast-rgb); 206 | --ion-color-shade: var(--ion-color-facebook-shade); 207 | --ion-color-tint: var(--ion-color-facebook-tint); 208 | } 209 | 210 | .ion-color-github { 211 | --ion-color-base: #211F1F; 212 | --ion-color-base-rgb: 33,31,31; 213 | --ion-color-contrast: #ffffff; 214 | --ion-color-contrast-rgb: 255,255,255; 215 | --ion-color-shade: #1d1b1b; 216 | --ion-color-tint: #373535; 217 | } 218 | 219 | .ion-color-instagram { 220 | --ion-color-base: #9537BC; 221 | --ion-color-base-rgb: 149,55,188; 222 | --ion-color-contrast: #ffffff; 223 | --ion-color-contrast-rgb: 255,255,255; 224 | --ion-color-shade: #8330a5; 225 | --ion-color-tint: #a04bc3; 226 | } 227 | 228 | /* 229 | * Shared Variables 230 | * ---------------------------------------------------------------------------- 231 | * To customize the look and feel of this app, you can override 232 | * the CSS variables found in Ionic's source files. 233 | * To view all of the possible Ionic variables, see: 234 | * https://ionicframework.com/docs/theming/css-variables#ionic-variables 235 | */ 236 | 237 | :root { 238 | --ion-headings-font-weight: 300; 239 | 240 | --ion-color-angular: #ac282b; 241 | --ion-color-communication: #8e8d93; 242 | --ion-color-tooling: #fe4c52; 243 | --ion-color-services: #fd8b2d; 244 | --ion-color-design: #fed035; 245 | --ion-color-workshop: #69bb7b; 246 | --ion-color-food: #3bc7c4; 247 | --ion-color-documentation: #b16be3; 248 | --ion-color-navigation: #6600cc; 249 | } 250 | 251 | /* 252 | * App iOS Variables 253 | * ---------------------------------------------------------------------------- 254 | * iOS only CSS variables can go here 255 | */ 256 | 257 | .ios { 258 | 259 | } 260 | 261 | /* 262 | * App Material Design Variables 263 | * ---------------------------------------------------------------------------- 264 | * Material Design only CSS variables can go here 265 | */ 266 | 267 | .md { 268 | 269 | } 270 | 271 | /* 272 | * App Theme 273 | * ---------------------------------------------------------------------------- 274 | * Ionic apps can have different themes applied, which can 275 | * then be further customized. These variables come last 276 | * so that the above variables are used by default. 277 | */ 278 | 279 | /* 280 | * Dark Theme 281 | * ---------------------------------------------------------------------------- 282 | */ 283 | 284 | .dark-theme { 285 | --ion-color-primary: #428cff; 286 | --ion-color-primary-rgb: 66,140,255; 287 | --ion-color-primary-contrast: #ffffff; 288 | --ion-color-primary-contrast-rgb: 255,255,255; 289 | --ion-color-primary-shade: #3a7be0; 290 | --ion-color-primary-tint: #5598ff; 291 | 292 | --ion-color-secondary: #50c8ff; 293 | --ion-color-secondary-rgb: 80,200,255; 294 | --ion-color-secondary-contrast: #ffffff; 295 | --ion-color-secondary-contrast-rgb: 255,255,255; 296 | --ion-color-secondary-shade: #46b0e0; 297 | --ion-color-secondary-tint: #62ceff; 298 | 299 | --ion-color-tertiary: #6a64ff; 300 | --ion-color-tertiary-rgb: 106,100,255; 301 | --ion-color-tertiary-contrast: #ffffff; 302 | --ion-color-tertiary-contrast-rgb: 255,255,255; 303 | --ion-color-tertiary-shade: #5d58e0; 304 | --ion-color-tertiary-tint: #7974ff; 305 | 306 | --ion-color-success: #2fdf75; 307 | --ion-color-success-rgb: 47,223,117; 308 | --ion-color-success-contrast: #000000; 309 | --ion-color-success-contrast-rgb: 0,0,0; 310 | --ion-color-success-shade: #29c467; 311 | --ion-color-success-tint: #44e283; 312 | 313 | --ion-color-warning: #ffd534; 314 | --ion-color-warning-rgb: 255,213,52; 315 | --ion-color-warning-contrast: #000000; 316 | --ion-color-warning-contrast-rgb: 0,0,0; 317 | --ion-color-warning-shade: #e0bb2e; 318 | --ion-color-warning-tint: #ffd948; 319 | 320 | --ion-color-danger: #ff4961; 321 | --ion-color-danger-rgb: 255,73,97; 322 | --ion-color-danger-contrast: #ffffff; 323 | --ion-color-danger-contrast-rgb: 255,255,255; 324 | --ion-color-danger-shade: #e04055; 325 | --ion-color-danger-tint: #ff5b71; 326 | 327 | --ion-color-dark: #f4f5f8; 328 | --ion-color-dark-rgb: 244,245,248; 329 | --ion-color-dark-contrast: #000000; 330 | --ion-color-dark-contrast-rgb: 0,0,0; 331 | --ion-color-dark-shade: #d7d8da; 332 | --ion-color-dark-tint: #f5f6f9; 333 | 334 | --ion-color-medium: #989aa2; 335 | --ion-color-medium-rgb: 152,154,162; 336 | --ion-color-medium-contrast: #000000; 337 | --ion-color-medium-contrast-rgb: 0,0,0; 338 | --ion-color-medium-shade: #86888f; 339 | --ion-color-medium-tint: #a2a4ab; 340 | 341 | --ion-color-light: #222428; 342 | --ion-color-light-rgb: 34,36,40; 343 | --ion-color-light-contrast: #ffffff; 344 | --ion-color-light-contrast-rgb: 255,255,255; 345 | --ion-color-light-shade: #1e2023; 346 | --ion-color-light-tint: #383a3e; 347 | } 348 | 349 | /* 350 | * iOS Dark Theme 351 | * ---------------------------------------------------------------------------- 352 | */ 353 | 354 | .dark-theme.ios { 355 | --ion-background-color: #000000; 356 | --ion-background-color-rgb: 0,0,0; 357 | 358 | --ion-text-color: #ffffff; 359 | --ion-text-color-rgb: 255,255,255; 360 | 361 | --ion-color-step-50: #0d0d0d; 362 | --ion-color-step-100: #1a1a1a; 363 | --ion-color-step-150: #262626; 364 | --ion-color-step-200: #333333; 365 | --ion-color-step-250: #404040; 366 | --ion-color-step-300: #4d4d4d; 367 | --ion-color-step-350: #595959; 368 | --ion-color-step-400: #666666; 369 | --ion-color-step-450: #737373; 370 | --ion-color-step-500: #808080; 371 | --ion-color-step-550: #8c8c8c; 372 | --ion-color-step-600: #999999; 373 | --ion-color-step-650: #a6a6a6; 374 | --ion-color-step-700: #b3b3b3; 375 | --ion-color-step-750: #bfbfbf; 376 | --ion-color-step-800: #cccccc; 377 | --ion-color-step-850: #d9d9d9; 378 | --ion-color-step-900: #e6e6e6; 379 | --ion-color-step-950: #f2f2f2; 380 | 381 | --ion-toolbar-background: #0d0d0d; 382 | 383 | --ion-item-background: #1c1c1c; 384 | --ion-item-background-activated: #313131; 385 | } 386 | 387 | 388 | /* 389 | * Material Design Dark Theme 390 | * ---------------------------------------------------------------------------- 391 | */ 392 | 393 | .dark-theme.md { 394 | --ion-background-color: #121212; 395 | --ion-background-color-rgb: 18,18,18; 396 | 397 | --ion-text-color: #ffffff; 398 | --ion-text-color-rgb: 255,255,255; 399 | 400 | --ion-border-color: #222222; 401 | 402 | --ion-color-step-50: #1e1e1e; 403 | --ion-color-step-100: #2a2a2a; 404 | --ion-color-step-150: #363636; 405 | --ion-color-step-200: #414141; 406 | --ion-color-step-250: #4d4d4d; 407 | --ion-color-step-300: #595959; 408 | --ion-color-step-350: #656565; 409 | --ion-color-step-400: #717171; 410 | --ion-color-step-450: #7d7d7d; 411 | --ion-color-step-500: #898989; 412 | --ion-color-step-550: #949494; 413 | --ion-color-step-600: #a0a0a0; 414 | --ion-color-step-650: #acacac; 415 | --ion-color-step-700: #b8b8b8; 416 | --ion-color-step-750: #c4c4c4; 417 | --ion-color-step-800: #d0d0d0; 418 | --ion-color-step-850: #dbdbdb; 419 | --ion-color-step-900: #e7e7e7; 420 | --ion-color-step-950: #f3f3f3; 421 | 422 | --ion-item-background: #1e1e1e; 423 | 424 | --ion-toolbar-background: #1f1f1f; 425 | 426 | --ion-tab-bar-background: #1f1f1f; 427 | } 428 | -------------------------------------------------------------------------------- /src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | (window as any).__Zone_disable_customElements = true; 6 | -------------------------------------------------------------------------------- /superstatic.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "**", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts", 13 | "src/**/*.d.ts" 14 | ], 15 | "exclude": [ 16 | "src/**/*.spec.ts", 17 | "src/test.ts", 18 | "src/environments/environment.prod.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": false, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "fullTemplateTypeCheck": true, 21 | "strictInjectionParameters": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 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 | { 34 | "order": [ 35 | "static-field", 36 | "instance-field", 37 | "static-method", 38 | "instance-method" 39 | ] 40 | } 41 | ], 42 | "no-arg": true, 43 | "no-bitwise": true, 44 | "no-console": [ 45 | true, 46 | "debug", 47 | "info", 48 | "time", 49 | "timeEnd", 50 | "trace" 51 | ], 52 | "no-construct": true, 53 | "no-debugger": true, 54 | "no-duplicate-super": true, 55 | "no-empty": false, 56 | "no-empty-interface": true, 57 | "no-eval": true, 58 | "no-inferrable-types": [ 59 | true, 60 | "ignore-params" 61 | ], 62 | "no-misused-new": true, 63 | "no-non-null-assertion": true, 64 | "no-shadowed-variable": true, 65 | "no-string-literal": false, 66 | "no-string-throw": true, 67 | "no-switch-case-fall-through": true, 68 | "no-trailing-whitespace": true, 69 | "no-unnecessary-initializer": true, 70 | "no-unused-expression": true, 71 | "no-use-before-declare": true, 72 | "no-var-keyword": true, 73 | "object-literal-sort-keys": false, 74 | "one-line": [ 75 | true, 76 | "check-open-brace", 77 | "check-catch", 78 | "check-else", 79 | "check-whitespace" 80 | ], 81 | "prefer-const": true, 82 | "quotemark": [ 83 | true, 84 | "single" 85 | ], 86 | "radix": true, 87 | "semicolon": [ 88 | true, 89 | "always" 90 | ], 91 | "triple-equals": [ 92 | true, 93 | "allow-null-check" 94 | ], 95 | "typedef-whitespace": [ 96 | true, 97 | { 98 | "call-signature": "nospace", 99 | "index-signature": "nospace", 100 | "parameter": "nospace", 101 | "property-declaration": "nospace", 102 | "variable-declaration": "nospace" 103 | } 104 | ], 105 | "unified-signatures": true, 106 | "variable-name": false, 107 | "whitespace": [ 108 | true, 109 | "check-branch", 110 | "check-decl", 111 | "check-operator", 112 | "check-separator", 113 | "check-type" 114 | ], 115 | "directive-selector": [ 116 | true, 117 | "attribute", 118 | "app", 119 | "camelCase" 120 | ], 121 | "component-selector": [ 122 | true, 123 | "element", 124 | "app", 125 | "page", 126 | "kebab-case" 127 | ], 128 | "no-output-on-prefix": true, 129 | "no-inputs-metadata-property": true, 130 | "no-outputs-metadata-property": true, 131 | "no-host-metadata-property": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-lifecycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "directive-class-suffix": true 137 | } 138 | } 139 | --------------------------------------------------------------------------------