├── .firebaserc ├── .gitignore ├── README.md ├── android ├── .gitignore ├── app │ ├── .npmignore │ ├── build.gradle │ ├── capacitor.build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── getcapacitor │ │ │ └── myapp │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ │ └── capacitor.config.json │ │ ├── java │ │ │ └── io │ │ │ │ └── ionic │ │ │ │ └── starter │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-land-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-land-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-port-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ ├── launch_splash.xml │ │ │ └── splash.png │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ ├── config.xml │ │ │ └── file_paths.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── getcapacitor │ │ └── myapp │ │ └── ExampleUnitTest.java ├── build.gradle ├── capacitor.settings.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── angular.json ├── browserlist ├── capacitor.config.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── firebase.json ├── functions ├── .gitignore ├── index.js ├── package-lock.json └── package.json ├── img ├── discover-place-detail.png ├── discover-places-page.png ├── firebase-database.png ├── my-offers-page.png └── new-offer.png ├── ionic.config.json ├── karma.conf.js ├── ngsw-config.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.guard.ts │ │ ├── auth.module.ts │ │ ├── auth.page.html │ │ ├── auth.page.scss │ │ ├── auth.page.spec.ts │ │ ├── auth.page.ts │ │ ├── auth.service.ts │ │ └── user.model.ts │ ├── bookings │ │ ├── booking.model.ts │ │ ├── booking.service.ts │ │ ├── bookings.module.ts │ │ ├── bookings.page.html │ │ ├── bookings.page.scss │ │ ├── bookings.page.spec.ts │ │ ├── bookings.page.ts │ │ └── create-booking │ │ │ ├── create-booking.component.html │ │ │ ├── create-booking.component.scss │ │ │ ├── create-booking.component.spec.ts │ │ │ └── create-booking.component.ts │ ├── places │ │ ├── discover │ │ │ ├── discover.module.ts │ │ │ ├── discover.page.html │ │ │ ├── discover.page.scss │ │ │ ├── discover.page.spec.ts │ │ │ ├── discover.page.ts │ │ │ └── place-detail │ │ │ │ ├── place-detail.module.ts │ │ │ │ ├── place-detail.page.html │ │ │ │ ├── place-detail.page.scss │ │ │ │ ├── place-detail.page.spec.ts │ │ │ │ └── place-detail.page.ts │ │ ├── location.model.ts │ │ ├── offers │ │ │ ├── edit-offer │ │ │ │ ├── edit-offer.module.ts │ │ │ │ ├── edit-offer.page.html │ │ │ │ ├── edit-offer.page.scss │ │ │ │ ├── edit-offer.page.spec.ts │ │ │ │ └── edit-offer.page.ts │ │ │ ├── new-offer │ │ │ │ ├── base64ToBlob.utility.ts │ │ │ │ ├── new-offer.module.ts │ │ │ │ ├── new-offer.page.html │ │ │ │ ├── new-offer.page.scss │ │ │ │ └── new-offer.page.ts │ │ │ ├── offer-item │ │ │ │ ├── offer-item.component.html │ │ │ │ ├── offer-item.component.scss │ │ │ │ ├── offer-item.component.spec.ts │ │ │ │ └── offer-item.component.ts │ │ │ ├── offers.module.ts │ │ │ ├── offers.page.html │ │ │ ├── offers.page.scss │ │ │ ├── offers.page.spec.ts │ │ │ └── offers.page.ts │ │ ├── place.model.ts │ │ ├── places-routing.module.ts │ │ ├── places.module.ts │ │ ├── places.page.html │ │ ├── places.page.scss │ │ ├── places.page.spec.ts │ │ ├── places.page.ts │ │ ├── places.service.spec.ts │ │ └── places.service.ts │ └── shared │ │ ├── map-modal │ │ ├── map-modal.component.html │ │ ├── map-modal.component.scss │ │ ├── map-modal.component.spec.ts │ │ └── map-modal.component.ts │ │ ├── pickers │ │ ├── image-picker │ │ │ ├── image-picker.component.html │ │ │ ├── image-picker.component.scss │ │ │ ├── image-picker.component.spec.ts │ │ │ └── image-picker.component.ts │ │ └── location-picker │ │ │ ├── location-picker.component.html │ │ │ ├── location-picker.component.scss │ │ │ ├── location-picker.component.spec.ts │ │ │ └── location-picker.component.ts │ │ └── shared.module.ts ├── assets │ ├── icon │ │ └── favicon.png │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ └── shapes.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── main.ts ├── manifest.webmanifest ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss └── zone-flags.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── typings └── cordova-typings.d.ts /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ionic-maps-api-1565705298126", 4 | "testing": "ionic-maps-api-1565705298126" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | # Specifies intentionally untracked files to ignore when using Git 3 | # http://git-scm.com/docs/gitignore 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 | 16 | .idea/ 17 | .ionic/ 18 | .sourcemaps/ 19 | .sass-cache/ 20 | .tmp/ 21 | .versions/ 22 | coverage/ 23 | www/ 24 | node_modules/ 25 | tmp/ 26 | temp/ 27 | platforms/ 28 | plugins/ 29 | plugins/android.json 30 | plugins/ios.json 31 | $RECYCLE.BIN/ 32 | 33 | .DS_Store 34 | Thumbs.db 35 | UserInterfaceState.xcuserstate 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :zap: Ionic Angular Project 2 | 3 | * App to create Airbnb-style property listings with pages to make bookings and update property details. Code from Udemy Tutorial: [Ionic 4 - Build iOS, Android & Web Apps with Ionic & Angular](https://www.udemy.com/ionic-2-the-practical-guide-to-building-ios-android-apps/), using the [Ionic 5 framework](https://ionicframework.com/docs). 4 | 5 | ## :page_facing_up: Table of contents 6 | 7 | * [:zap: Ionic Angular Project](#zap-ionic-angular-project) 8 | * [:page_facing_up: Table of contents](#page_facing_up-table-of-contents) 9 | * [:books: General info](#books-general-info) 10 | * [RxJS operators](#rxjs-operators) 11 | * [:books: Ionic Controllers Used](#books-ionic-controllers-used) 12 | * [:books: Observables](#books-observables) 13 | * [:books: Array Operators](#books-array-operators) 14 | * [:camera: Screenshots](#camera-screenshots) 15 | * [:signal_strength: Technologies](#signal_strength-technologies) 16 | * [:floppy_disk: Setup](#floppy_disk-setup) 17 | * [:computer: Code Examples (taken from Udemy course with my comments added)](#computer-code-examples-taken-from-udemy-course-with-my-comments-added) 18 | * [:cool: Features](#cool-features) 19 | * [:clipboard: Status & To-do list](#clipboard-status--to-do-list) 20 | * [:clap: Inspiration](#clap-inspiration) 21 | * [:file_folder: License](#file_folder-license) 22 | * [:envelope: Contact](#envelope-contact) 23 | 24 | ## :books: General info 25 | 26 | * App to view and book places to stay. All places are listed on the 'discover.page' and clicking on an item navigates to a place detail page using the place id in the browser. 27 | * Places are displayed under 2 list options: 'ALL PLACES' and 'BOOKABLE PLACES'. The first place is displayed using an ion-card, the remaining places are displayed using a list with a thumbnail image. There is code to prevent the user from being able to book their own places, using a userId matching function to show/hide the booking button. 28 | * Places can be booked, listed and cancelled. 29 | * New places can be added as 'Offers'. The location of the new place is chosen using the Google Maps API and is displayed in the template using data-binding. A photo can be taken to add to the new Place description. If there is no camera then there is a file upload button to save a jpeg image. 30 | * Burger side panel added with links to the discover places listings, your bookings and a logout button. 31 | * Bottom menu with 2 links to 'Discover' (default page upon loading) and 'Offers' that lists all the places available. 32 | 33 | ## [RxJS operators](http://reactivex.io/documentation/observable.html) 34 | 35 | * **general** all operators return observables. You have to subscribe to observables. 36 | * **switchMap** for http requests that emit just one value and for long-lived streams for Firebase real-time database and authentication. They do not need to be unsubscribed as they complete after emission. **switch:** because the result observable has switched from emitting the values of the first inner observable to emitting the values of the newly created inner (derived) observable. The previous inner observable is cancelled and the new observable is subscribed. **map:** because what is being mapped is the emitted source value, that is getting mapped to an observable using the mapping function passed to switchMap. (The alternative operator is mergeMap). 37 | * **of** used with a single value for an 'emit once and complete' stream. 38 | * **take** emits only the first n values from an observable (e.g. take(1) emits only the first 2 values ) 39 | * **tap** used to perform side effects. Every data value is received from the source, an action is taken on a part of the data then the data is passed on unchanged. 40 | * **map** transforms things. It passes each source value through a transformation function then outputs the results, e.g map(x => 10*x). 41 | * **pipe** composes operators. Creates a pipeline of small reusable operators like map and filter. 42 | * **from** converts a mix of other objects and data types into Observables. 43 | 44 | ## :books: Ionic Controllers Used 45 | 46 | * [Alert Controller](https://ionicframework.com/docs/api/alert) alert appears on top of app contents. 47 | * [Loading Controller](https://ionicframework.com/docs/api/loading) overlay used to display activity and block user input. Loading indicators can be created, including spinners. 48 | 49 | ## :books: Observables 50 | 51 | * An [observable](https://rxjs-dev.firebaseapp.com/guide/observable) is created using 'new Observable'. It is subscribed to using an Observer and executed to deliver next / error / complete notices to the Observer, before the execution is disposed of. Subscribers should be wrapped in try/catch blocks. 52 | * a [BehaviourSubject](http://reactivex.io/rxjs/manual/overview.html#behaviorsubject) is a subject that requires an initial value and emits its current value to subscribers. 53 | 54 | ## :books: Array Operators 55 | 56 | * [Array.push()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push) adds one or more elements to the end of an array and returns the new array length. 57 | 58 | ## :camera: Screenshots 59 | 60 | ![page](./img/discover-places-page.png) 61 | ![page](./img/discover-place-detail.png) 62 | ![page](./img/new-offer.png) 63 | ![page](./img/my-offers-page.png) 64 | ![page](./img/firebase-database.png) 65 | 66 | ## :signal_strength: Technologies 67 | 68 | * [Ionic v6](https://ionicframework.com/) 69 | * [Angular v13](https://angular.io/) 70 | * [Ionic/angular v6](https://ionicframework.com/) 71 | * [RxJS v7](https://angular.io/guide/rx-library) 72 | * [Google Firebase](https://firebase.google.com) 73 | * [Google Maps Javascript API](https://developers.google.com/maps/documentation/javascript/tutorial) 74 | * [Capacitor v2](https://capacitor.ionicframework.com/) 75 | * [Ionic PWA Elements](https://www.npmjs.com/package/@ionic/pwa-elements) 76 | 77 | ## :floppy_disk: Setup 78 | 79 | * Add Google maps and firebase API keys to environment.ts 80 | * To start the server on _localhost://8100_ type: `ionic serve` 81 | * To generate normal www build file: `ionic build` 82 | * Build for Android app: `ionic capacitor run android` 83 | 84 | ## :computer: Code Examples (taken from Udemy course with my comments added) 85 | 86 | * extract from `map.modal.component.ts` - function to show location using google Maps API. 87 | 88 | ```typescript 89 | private getGoogleMaps(): Promise { 90 | const win = window as any; 91 | const googleModule = win.google; 92 | 93 | // check if google maps loaded already, if so go to google maps module 94 | if (googleModule && googleModule.maps) { 95 | return Promise.resolve(googleModule.maps); 96 | } 97 | 98 | // show google maps window as a DOM child script 99 | return new Promise((resolve, reject) => { 100 | const script = document.createElement('script'); 101 | script.src = 'https://maps.googleapis.com/maps/api/js?key=' + environment.googleMapsAPIKey; 102 | script.async = true; 103 | script.defer = true; 104 | document.body.appendChild(script); 105 | 106 | // script listener is an anonymous arrow function 107 | script.onload = () => { 108 | const loadedGoogleModule = win.google; 109 | if (loadedGoogleModule && loadedGoogleModule.maps) { 110 | resolve(loadedGoogleModule.maps); 111 | } else { 112 | reject ('Google Maps SDK not available'); 113 | } 114 | }; 115 | }); 116 | } 117 | ``` 118 | 119 | * Firebase database setup: `".indexOn": ["userId"]` added to allow ordering by userId in `booking.service.ts` 120 | 121 | ```json 122 | { 123 | "rules": { 124 | ".read": true, 125 | ".write": true, 126 | "bookings": { 127 | ".indexOn": ["userId"] 128 | } 129 | } 130 | } 131 | 132 | ``` 133 | 134 | ## :cool: Features 135 | 136 | * Authorization module using Angular Routing with the Angular [canLoad auth guard interface](https://angular.io/api/router/CanLoad) to prevent access to pages if user is not logged in. 137 | * [Theme variables.scss](https://ionicframework.com/docs/theming/css-variables) file used to create a global color theme using the [Ionic color palette](https://ionicframework.com/docs/theming/color-generator) (note colors were in rgb not #hex as shown in the Ionic tutorial). 138 | * [Ionic datetime picker interface](https://ionicframework.com/docs/api/datetime) used to select booking dates. Alternative is a random dates option. 139 | * [RxJS](https://angular.io/guide/rx-library) reactive programming used to manage state. 140 | * Error handling added 141 | * [Firebase backend database](https://firebase.google.com) used to store place and booking data. Images are stored in the same database. 142 | * Bookings can be cancelled from booking.page. 143 | * Bookings can be made and new places added to offers page. If a different user is logged in they cannot book their own places (the booking button does not show) - which is correct. 144 | * Camera images now show. 145 | * Place details can be edited (as long as user id matches) using a neat button that slides from the right. 146 | * [Google Maps Javascript API](https://developers.google.com/maps/documentation/javascript/tutorial) map-modal added to new-offer page. Clicking on 'SELECT LOCATION' will open Google Maps at a fixed location. Address of place extracted from Google Maps data and stored in Places database. 147 | * [Capacitor Geolocation API](https://capacitor.ionicframework.com/docs/apis/geolocation) used to provide current location. 148 | * [Capacitor Camera API](https://capacitor.ionicframework.com/docs/apis/camera) used to provide camera functionality. 149 | * [Capacitor Local Storage](https://capacitor.ionicframework.com/docs/apis/storage/) API provides a key-value store for simple data. Used to save user authentication token so a refresh etc. does not lose a user's settings. 150 | * [Google Cloud Storage](https://www.npmjs.com/package/@google-cloud/storage) used for storage of image data. 151 | * Auth tokens on the backend. 152 | 153 | ## :clipboard: Status & To-do list 154 | 155 | * Status: Updated to latest Ionic & Angular versions. Not fully tested since updates. 156 | * To-do: Bookable place list is the same as the 'All Places' list - bookable places should not include the logged in users' places. 'My Offers' includes everyones places. 157 | 158 | ## :clap: Inspiration 159 | 160 | [Acadamind Udemy Course: Ionic 4 - Build iOS, Android & Web Apps with Ionic & Angular](https://www.udemy.com/ionic-2-the-practical-guide-to-building-ios-android-apps/) 161 | 162 | ## :file_folder: License 163 | 164 | * N/A 165 | 166 | ## :envelope: Contact 167 | 168 | * Repo created by [ABateman](https://github.com/AndrewJBateman), email: gomezbateman@yahoo.com -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # NPM renames .gitignore to .npmignore 2 | # In order to prevent that, we remove the initial "." 3 | # And the CLI then renames it 4 | 5 | # Using Android gitignore template: https://github.com/github/gitignore/blob/master/Android.gitignore 6 | 7 | # Built application files 8 | *.apk 9 | *.ap_ 10 | *.aab 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | release/ 23 | 24 | # Gradle files 25 | .gradle/ 26 | build/ 27 | 28 | # Local configuration file (sdk path, etc) 29 | local.properties 30 | 31 | # Proguard folder generated by Eclipse 32 | proguard/ 33 | 34 | # Log Files 35 | *.log 36 | 37 | # Android Studio Navigation editor temp files 38 | .navigation/ 39 | 40 | # Android Studio captures folder 41 | captures/ 42 | 43 | # IntelliJ 44 | *.iml 45 | .idea/workspace.xml 46 | .idea/tasks.xml 47 | .idea/gradle.xml 48 | .idea/assetWizardSettings.xml 49 | .idea/dictionaries 50 | .idea/libraries 51 | # Android Studio 3 in .gitignore file. 52 | .idea/caches 53 | .idea/modules.xml 54 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 55 | .idea/navEditor.xml 56 | 57 | # Keystore files 58 | # Uncomment the following lines if you do not want to check your keystore files in. 59 | #*.jks 60 | #*.keystore 61 | 62 | # External native build folder generated in Android Studio 2.2 and later 63 | .externalNativeBuild 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | # Cordova plugins for Capacitor 88 | capacitor-cordova-android-plugins 89 | 90 | # Copied web assets 91 | app/src/main/assets/public 92 | -------------------------------------------------------------------------------- /android/app/.npmignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !/build/.npmkeep 3 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "io.ionic.starter" 7 | minSdkVersion 21 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | repositories { 22 | maven { 23 | url "https://dl.bintray.com/ionic-team/capacitor" 24 | } 25 | flatDir{ 26 | dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(include: ['*.jar'], dir: 'libs') 32 | implementation 'com.android.support:appcompat-v7:28.0.0' 33 | implementation project(':capacitor-android') 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 36 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 37 | implementation project(':capacitor-cordova-android-plugins') 38 | } 39 | 40 | apply from: 'capacitor.build.gradle' 41 | 42 | try { 43 | def servicesJSON = file('google-services.json') 44 | if (servicesJSON.text) { 45 | apply plugin: 'com.google.gms.google-services' 46 | } 47 | } catch(Exception e) { 48 | logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work") 49 | } -------------------------------------------------------------------------------- /android/app/capacitor.build.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility JavaVersion.VERSION_1_8 6 | targetCompatibility JavaVersion.VERSION_1_8 7 | } 8 | } 9 | 10 | apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" 11 | dependencies { 12 | 13 | 14 | } 15 | 16 | 17 | if (hasProperty('postBuildExtras')) { 18 | postBuildExtras() 19 | } 20 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.getcapacitor.app", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /android/app/src/main/assets/capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "io.ionic.starter", 3 | "appName": "ionic-angular-project", 4 | "bundledWebRuntime": false, 5 | "npmClient": "npm", 6 | "webDir": "www" 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/ionic/starter/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.ionic.starter; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.getcapacitor.BridgeActivity; 6 | import com.getcapacitor.Plugin; 7 | 8 | import java.util.ArrayList; 9 | 10 | public class MainActivity extends BridgeActivity { 11 | @Override 12 | public void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | 15 | // Initializes the Bridge 16 | this.init(savedInstanceState, new ArrayList>() {{ 17 | // Additional plugins you've installed go here 18 | // Ex: add(TotallyAwesomePlugin.class); 19 | }}); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-land-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-land-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-land-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-land-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-land-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-port-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-port-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-port-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-port-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable-port-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/drawable/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ionic-angular-project 4 | ionic-angular-project 5 | io.ionic.starter 6 | io.ionic.starter.fileprovider 7 | io.ionic.starter 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.3.2' 11 | classpath 'com.google.gms:google-services:4.2.0' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /android/capacitor.settings.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | include ':capacitor-android' 3 | project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') 4 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jan 30 13:14:22 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':capacitor-cordova-android-plugins' 3 | project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') 4 | 5 | apply from: 'capacitor.settings.gradle' -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", 3 | "version": 1, 4 | "defaultProject": "app", 5 | "newProjectRoot": "projects", 6 | "projects": { 7 | "app": { 8 | "root": "", 9 | "sourceRoot": "src", 10 | "projectType": "application", 11 | "prefix": "app", 12 | "schematics": {}, 13 | "architect": { 14 | "build": { 15 | "builder": "@angular-devkit/build-angular:browser", 16 | "options": { 17 | "outputPath": "www", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "tsconfig.app.json", 22 | "assets": [ 23 | { 24 | "glob": "**/*", 25 | "input": "src/assets", 26 | "output": "assets" 27 | }, 28 | { 29 | "glob": "**/*.svg", 30 | "input": "node_modules/ionicons/dist/ionicons/svg", 31 | "output": "./svg" 32 | }, 33 | "src/manifest.webmanifest" 34 | ], 35 | "styles": [ 36 | { 37 | "input": "src/theme/variables.scss", 38 | "inject": true 39 | }, 40 | { 41 | "input": "src/global.scss", 42 | "inject": true 43 | } 44 | ], 45 | "scripts": [], 46 | "aot": false, 47 | "vendorChunk": true, 48 | "extractLicenses": false, 49 | "buildOptimizer": false, 50 | "sourceMap": true, 51 | "optimization": false, 52 | "namedChunks": true 53 | }, 54 | "configurations": { 55 | "production": { 56 | "fileReplacements": [ 57 | { 58 | "replace": "src/environments/environment.ts", 59 | "with": "src/environments/environment.prod.ts" 60 | } 61 | ], 62 | "optimization": true, 63 | "outputHashing": "all", 64 | "sourceMap": false, 65 | "namedChunks": false, 66 | "aot": true, 67 | "extractLicenses": true, 68 | "vendorChunk": false, 69 | "buildOptimizer": true, 70 | "budgets": [ 71 | { 72 | "type": "initial", 73 | "maximumWarning": "2mb", 74 | "maximumError": "5mb" 75 | } 76 | ], 77 | "serviceWorker": true, 78 | "ngswConfigPath": "ngsw-config.json" 79 | }, 80 | "ci": { 81 | "progress": false 82 | } 83 | }, 84 | "defaultConfiguration": "" 85 | }, 86 | "serve": { 87 | "builder": "@angular-devkit/build-angular:dev-server", 88 | "options": { 89 | "browserTarget": "app:build" 90 | }, 91 | "configurations": { 92 | "production": { 93 | "browserTarget": "app:build:production" 94 | }, 95 | "ci": { 96 | } 97 | } 98 | }, 99 | "extract-i18n": { 100 | "builder": "@angular-devkit/build-angular:extract-i18n", 101 | "options": { 102 | "browserTarget": "app:build" 103 | } 104 | }, 105 | "test": { 106 | "builder": "@angular-devkit/build-angular:karma", 107 | "options": { 108 | "main": "src/test.ts", 109 | "polyfills": "src/polyfills.ts", 110 | "tsConfig": "tsconfig.spec.json", 111 | "karmaConfig": "karma.conf.js", 112 | "styles": [], 113 | "scripts": [], 114 | "assets": [ 115 | { 116 | "glob": "favicon.ico", 117 | "input": "src/", 118 | "output": "/" 119 | }, 120 | { 121 | "glob": "**/*", 122 | "input": "src/assets", 123 | "output": "/assets" 124 | }, 125 | "src/manifest.webmanifest" 126 | ] 127 | }, 128 | "configurations": { 129 | "ci": { 130 | "progress": false, 131 | "watch": false 132 | } 133 | } 134 | }, 135 | "e2e": { 136 | "builder": "@angular-devkit/build-angular:protractor", 137 | "options": { 138 | "protractorConfig": "e2e/protractor.conf.js", 139 | "devServerTarget": "app:serve" 140 | }, 141 | "configurations": { 142 | "production": { 143 | "devServerTarget": "app:serve:production" 144 | }, 145 | "ci": { 146 | "devServerTarget": "app:serve:ci" 147 | } 148 | } 149 | }, 150 | "ionic-cordova-build": { 151 | "builder": "@ionic/angular-toolkit:cordova-build", 152 | "options": { 153 | "browserTarget": "app:build" 154 | }, 155 | "configurations": { 156 | "production": { 157 | "browserTarget": "app:build:production" 158 | } 159 | } 160 | }, 161 | "ionic-cordova-serve": { 162 | "builder": "@ionic/angular-toolkit:cordova-serve", 163 | "options": { 164 | "cordovaBuildTarget": "app:ionic-cordova-build", 165 | "devServerTarget": "app:serve" 166 | }, 167 | "configurations": { 168 | "production": { 169 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 170 | "devServerTarget": "app:serve:production" 171 | } 172 | } 173 | } 174 | } 175 | } 176 | }, 177 | "cli": { 178 | "defaultCollection": "@ionic/angular-toolkit" 179 | }, 180 | "schematics": { 181 | "@ionic/angular-toolkit:component": { 182 | "styleext": "scss" 183 | }, 184 | "@ionic/angular-toolkit:page": { 185 | "styleext": "scss" 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /browserlist: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "io.ionic.starter", 3 | "appName": "ionic-angular-project", 4 | "bundledWebRuntime": false, 5 | "npmClient": "npm", 6 | "webDir": "www" 7 | } 8 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should be blank', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('The world is your oyster.'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.deepCss('app-root ion-content')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const cors = require('cors')({ origin: true }); 3 | const Busboy = require('busboy'); 4 | const os = require('os'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const uuid = require('uuid/v4'); 8 | const fbAdmin = require('firebase-admin'); 9 | 10 | const { Storage } = require('@google-cloud/storage'); 11 | 12 | const storage = new Storage({ 13 | projectId: 'ionic-maps-api-1565705298126', 14 | }); 15 | 16 | fbAdmin.initializeApp({ 17 | credential: fbAdmin.credential.cert(require('')), 18 | }); 19 | 20 | exports.storeImage = functions.https.onRequest((req, res) => { 21 | return cors(req, res, () => { 22 | if (req.method !== 'POST') { 23 | return res.status(500).json({ message: 'Not allowed.' }); 24 | } 25 | 26 | if ( 27 | !req.headers.authorization || 28 | !req.headers.authorization.startsWith('Bearer ') 29 | ) { 30 | return res.status(401).json({ error: 'Unauthorized!' }); 31 | } 32 | 33 | let idToken; 34 | idToken = req.headers.authorization.split('Bearer ')[1]; 35 | 36 | const busboy = new Busboy({ headers: req.headers }); 37 | let uploadData; 38 | let oldImagePath; 39 | 40 | busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { 41 | const filePath = path.join(os.tmpdir(), filename); 42 | uploadData = { filePath: filePath, type: mimetype, name: filename }; 43 | file.pipe(fs.createWriteStream(filePath)); 44 | }); 45 | 46 | busboy.on('field', (fieldname, value) => { 47 | oldImagePath = decodeURIComponent(value); 48 | }); 49 | 50 | busboy.on('finish', () => { 51 | const id = uuid(); 52 | let imagePath = 'images/' + id + '-' + uploadData.name; 53 | if (oldImagePath) { 54 | imagePath = oldImagePath; 55 | } 56 | 57 | return fbAdmin 58 | .auth() 59 | .verifyIdToken(idToken) 60 | .then((decodedToken) => { 61 | console.log(uploadData.type); 62 | return storage 63 | .bucket('ionic-maps-api-1565705298126.appspot.com') 64 | .upload(uploadData.filePath, { 65 | uploadType: 'media', 66 | destination: imagePath, 67 | metadata: { 68 | metadata: { 69 | contentType: uploadData.type, 70 | firebaseStorageDownloadTokens: id, 71 | }, 72 | }, 73 | }); 74 | }) 75 | .then(() => { 76 | return res.status(201).json({ 77 | imageUrl: 78 | 'https://firebasestorage.googleapis.com/v0/b/' + 79 | storage.bucket('ionic-maps-api-1565705298126.appspot.com').name + 80 | '/o/' + 81 | encodeURIComponent(imagePath) + 82 | '?alt=media&token=' + 83 | id, 84 | imagePath: imagePath, 85 | }); 86 | }) 87 | .catch((error) => { 88 | console.log(error); 89 | return res.status(401).json({ error: 'Unauthorized!' }); 90 | }); 91 | }); 92 | return busboy.end(req.rawBody); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "8" 13 | }, 14 | "dependencies": { 15 | "firebase-admin": "^9.4.1", 16 | "firebase-functions": "^3.11.0", 17 | "@google-cloud/storage": "5.5.0", 18 | "busboy": "0.3.1", 19 | "cors": "2.8.5", 20 | "uuid": "8.3.1" 21 | }, 22 | "devDependencies": { 23 | "firebase-functions-test": "^0.2.3" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /img/discover-place-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/img/discover-place-detail.png -------------------------------------------------------------------------------- /img/discover-places-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/img/discover-places-page.png -------------------------------------------------------------------------------- /img/firebase-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/img/firebase-database.png -------------------------------------------------------------------------------- /img/my-offers-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/img/my-offers-page.png -------------------------------------------------------------------------------- /img/new-offer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/img/new-offer.png -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-angular-project", 3 | "integrations": { 4 | "capacitor": {} 5 | }, 6 | "type": "angular" 7 | } 8 | -------------------------------------------------------------------------------- /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', 'text-summary'], 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 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/*.css", 13 | "/*.js" 14 | ] 15 | } 16 | }, { 17 | "name": "assets", 18 | "installMode": "lazy", 19 | "updateMode": "prefetch", 20 | "resources": { 21 | "files": [ 22 | "/assets/**", 23 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 24 | ] 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-angular-project", 3 | "version": "0.0.1", 4 | "author": "Ionic Framework", 5 | "homepage": "https://ionicframework.com/", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "~13.2.4", 17 | "@angular/compiler": "~13.2.4", 18 | "@angular/core": "~13.2.4", 19 | "@angular/forms": "~13.2.4", 20 | "@angular/platform-browser": "~13.2.4", 21 | "@angular/platform-browser-dynamic": "~13.2.4", 22 | "@angular/pwa": "^0.1000.2", 23 | "@angular/router": "~13.2.4", 24 | "@angular/service-worker": "~13.2.4", 25 | "@capacitor/android": "^2.2.1", 26 | "@capacitor/core": "2.2.1", 27 | "@ionic-native/core": "^5.27.0", 28 | "@ionic-native/splash-screen": "^5.27.0", 29 | "@ionic-native/status-bar": "^5.27.0", 30 | "@ionic/angular": "^5.2.3", 31 | "@ionic/pwa-elements": "^1.5.2", 32 | "core-js": "^3.6.5", 33 | "rxjs": "~6.6.0", 34 | "tslib": "^2.0.0", 35 | "zone.js": "~0.11.4" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/architect": "~0.1302.5", 39 | "@angular-devkit/build-angular": "~13.2.5", 40 | "@angular-devkit/core": "~13.2.5", 41 | "@angular-devkit/schematics": "^13.2.5", 42 | "@angular/cli": "~13.2.5", 43 | "@angular/compiler-cli": "~13.2.4", 44 | "@angular/language-service": "~13.2.4", 45 | "@capacitor/cli": "2.2.1", 46 | "@ionic/angular-toolkit": "~2.2.0", 47 | "@types/jasmine": "~3.6.0", 48 | "@types/jasminewd2": "~2.0.8", 49 | "@types/node": "~14.0.22", 50 | "codelyzer": "^6.0.0", 51 | "jasmine-core": "~3.6.0", 52 | "jasmine-spec-reporter": "~5.0.0", 53 | "karma": "~6.3.16", 54 | "karma-chrome-launcher": "~3.1.0", 55 | "karma-coverage-istanbul-reporter": "~3.0.3", 56 | "karma-jasmine": "~4.0.0", 57 | "karma-jasmine-html-reporter": "^1.5.0", 58 | "protractor": "~7.0.0", 59 | "ts-node": "~8.10.2", 60 | "typescript": "4.5.5" 61 | }, 62 | "description": "An Ionic project" 63 | } -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | import { AuthGuard } from './auth/auth.guard'; 4 | 5 | const routes: Routes = [ 6 | { path: '', redirectTo: 'places', pathMatch: 'full' }, 7 | { path: 'auth', loadChildren: () => import('./auth/auth.module').then(m => m.AuthPageModule) }, 8 | { 9 | path: 'places', 10 | loadChildren: () => import('./places/places.module').then(m => m.PlacesPageModule), 11 | canLoad: [AuthGuard] 12 | }, 13 | { 14 | path: 'bookings', 15 | loadChildren: () => import('./bookings/bookings.module').then(m => m.BookingsPageModule), 16 | canLoad: [AuthGuard] 17 | }, 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [ 22 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules, relativeLinkResolution: 'legacy' }) 23 | ], 24 | exports: [RouterModule] 25 | }) 26 | export class AppRoutingModule { } 27 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PairBnB 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Discover Places 17 | 18 | 19 | 20 | 21 | Your Bookings 22 | 23 | 24 | 25 | 26 | Logout 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | // App Styles 2 | // ---------------------------------------------------------------------------- 3 | // Put style rules here that you want to apply to the entire application. These 4 | // styles are for the entire app and not just one component. Additionally, this 5 | // file can hold Sass mixins, functions, and placeholder classes to be imported 6 | // and used throughout the application. -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { Platform } from '@ionic/angular'; 5 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 6 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 7 | 8 | import { AppComponent } from './app.component'; 9 | 10 | describe('AppComponent', () => { 11 | 12 | let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy; 13 | 14 | beforeEach(waitForAsync(() => { 15 | statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']); 16 | splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']); 17 | platformReadySpy = Promise.resolve(); 18 | platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy }); 19 | 20 | TestBed.configureTestingModule({ 21 | declarations: [AppComponent], 22 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 23 | providers: [ 24 | { provide: StatusBar, useValue: statusBarSpy }, 25 | { provide: SplashScreen, useValue: splashScreenSpy }, 26 | { provide: Platform, useValue: platformSpy }, 27 | ], 28 | }).compileComponents(); 29 | })); 30 | 31 | it('should create the app', () => { 32 | const fixture = TestBed.createComponent(AppComponent); 33 | const app = fixture.debugElement.componentInstance; 34 | expect(app).toBeTruthy(); 35 | }); 36 | 37 | it('should initialize the app', async () => { 38 | TestBed.createComponent(AppComponent); 39 | expect(platformSpy.ready).toHaveBeenCalled(); 40 | await platformReadySpy; 41 | expect(statusBarSpy.styleDefault).toHaveBeenCalled(); 42 | expect(splashScreenSpy.hide).toHaveBeenCalled(); 43 | }); 44 | 45 | // TODO: add more tests! 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Platform } from '@ionic/angular'; 3 | import { Plugins, Capacitor, AppState } from '@capacitor/core'; 4 | 5 | import { Router } from '@angular/router'; 6 | 7 | import { AuthService } from './auth/auth.service'; 8 | import { Subscription } from 'rxjs'; 9 | import { take } from 'rxjs/operators'; 10 | 11 | 12 | @Component({ 13 | selector: 'app-root', 14 | templateUrl: 'app.component.html', 15 | styleUrls: ['app.component.scss'] 16 | }) 17 | export class AppComponent implements OnInit, OnDestroy { 18 | private authSub: Subscription; 19 | private previousAuthState = false; 20 | 21 | constructor( 22 | private platform: Platform, 23 | private authService: AuthService, 24 | private router: Router 25 | ) { 26 | this.initializeApp(); 27 | } 28 | 29 | initializeApp() { 30 | this.platform.ready().then(() => { 31 | if (Capacitor.isPluginAvailable('Splashscreen')) { 32 | Plugins.Splashscreen.hide(); 33 | } 34 | }); 35 | } 36 | 37 | ngOnInit() { 38 | this.authSub = this.authService.userIsAuthenticated.subscribe(isAuth => { 39 | if (!isAuth && this.previousAuthState !== isAuth) { 40 | this.router.navigateByUrl('/auth'); 41 | } 42 | this.previousAuthState = isAuth; 43 | }); 44 | Plugins.App.addListener('appStateChange', this.checkAuthOnResume.bind(this)); 45 | } 46 | 47 | onLogout() { 48 | this.authService.logout(); 49 | } 50 | 51 | ngOnDestroy() { 52 | if (this.authSub) { 53 | this.authSub.unsubscribe(); 54 | } 55 | } 56 | 57 | private checkAuthOnResume(state: AppState) { 58 | if (state.isActive) { 59 | this.authService 60 | .autoLogin() 61 | .pipe(take(1)) 62 | .subscribe(success => { 63 | if (!success) { 64 | this.onLogout(); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { RouteReuseStrategy } from '@angular/router'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | 7 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 8 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 9 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 10 | 11 | import { AppComponent } from './app.component'; 12 | import { AppRoutingModule } from './app-routing.module'; 13 | import { ServiceWorkerModule } from '@angular/service-worker'; 14 | import { environment } from '../environments/environment'; 15 | 16 | @NgModule({ 17 | declarations: [AppComponent], 18 | imports: [BrowserModule, HttpClientModule, IonicModule.forRoot(), AppRoutingModule, ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })], 19 | providers: [ 20 | StatusBar, 21 | SplashScreen, 22 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } 23 | ], 24 | bootstrap: [AppComponent] 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /src/app/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanLoad, Route, UrlSegment, Router } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | 5 | import { AuthService } from './auth.service'; 6 | import { take, tap, switchMap } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AuthGuard implements CanLoad { 12 | constructor(private authService: AuthService, private router: Router) {} 13 | 14 | canLoad( 15 | route: Route, 16 | segments: UrlSegment[] 17 | ): Observable | Promise | boolean { 18 | return this.authService.userIsAuthenticated.pipe( 19 | take(1), 20 | switchMap(isAuthenticated => { 21 | if (!isAuthenticated) { 22 | return this.authService.autoLogin(); 23 | } else { 24 | return of(isAuthenticated); 25 | } 26 | }), 27 | tap(isAuthenticated => { 28 | if (!isAuthenticated) { 29 | this.router.navigateByUrl('/auth'); 30 | } 31 | }) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { AuthPage } from './auth.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: AuthPage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [AuthPage] 25 | }) 26 | export class AuthPageModule {} 27 | -------------------------------------------------------------------------------- /src/app/auth/auth.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ isLogin ? 'Login' : 'Signup' }} 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | E-Mail 18 | 26 | 27 | 30 | 31 | Should be a valid email address. 32 | 33 | 34 | 35 | 36 | 37 | Password 38 | 46 | 47 | 50 | 51 | Should be at least 6 characters long. 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 68 | Switch to {{ isLogin ? 'Signup' : 'Login' }} 69 | 70 | 75 | {{ isLogin ? 'Login' : 'Signup' }} 76 | 77 | 78 | 79 | 80 |
81 | 82 |
83 | -------------------------------------------------------------------------------- /src/app/auth/auth.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/auth/auth.page.scss -------------------------------------------------------------------------------- /src/app/auth/auth.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { AuthPage } from './auth.page'; 5 | 6 | describe('AuthPage', () => { 7 | let component: AuthPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ AuthPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(AuthPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/auth/auth.page.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { NgForm } from '@angular/forms'; 5 | import { LoadingController, AlertController } from '@ionic/angular'; 6 | 7 | import { AuthService, AuthResponseData } from './auth.service'; 8 | 9 | @Component({ 10 | selector: 'app-auth', 11 | templateUrl: './auth.page.html', 12 | styleUrls: ['./auth.page.scss'], 13 | }) 14 | export class AuthPage implements OnInit { 15 | isLoading = false; 16 | isLogin = true; 17 | 18 | constructor( 19 | private authService: AuthService, 20 | private router: Router, 21 | private loadingCtrl: LoadingController, 22 | private alertCtrl: AlertController 23 | ) { } 24 | 25 | ngOnInit() { 26 | } 27 | 28 | authenticate(email: string, password: string) { 29 | this.isLoading = true; 30 | this.loadingCtrl 31 | .create({ keyboardClose: true, message: 'Logging in...' }) 32 | .then(loadingEl => { 33 | loadingEl.present(); // mask over screen while loading 34 | let authObs: Observable; 35 | if (this.isLogin) { 36 | authObs = this.authService.login(email, password); 37 | } else { 38 | authObs = this.authService.signup(email, password); 39 | } 40 | authObs.subscribe( 41 | resData => { 42 | this.isLoading = false; 43 | loadingEl.dismiss(); 44 | this.router.navigateByUrl('/places/tabs/discover'); 45 | }, 46 | errRes => { 47 | loadingEl.dismiss(); 48 | const code = errRes.error.error.message; 49 | let message = 'Could not sign you up, try again'; 50 | if (code === 'EMAIL_EXISTS') { 51 | message = 'This email address already exists'; 52 | } else if (code === 'EMAIL_NOT_FOUND') { 53 | message = 'email address could not be found'; 54 | } else if (code === 'INVALID_PASSWORD') { 55 | message = 'This password is not correct'; 56 | } 57 | this.showAlert(message); 58 | } 59 | ); 60 | }); 61 | } 62 | 63 | // toggle boolean value to switch between login and signup button/title in template 64 | onSwitchAuthMode() { 65 | this.isLogin = !this.isLogin; 66 | } 67 | 68 | onSubmit(form: NgForm) { 69 | if (!form.valid) { 70 | return; 71 | } 72 | const email = form.value.email; 73 | const password = form.value.password; 74 | 75 | this.authenticate(email, password); 76 | form.reset(); 77 | } 78 | 79 | private showAlert(message: string) { 80 | this.alertCtrl 81 | .create({ 82 | header: 'Authentication failed', 83 | message, 84 | buttons: ['OK'] 85 | }) 86 | .then(alertEl => alertEl.present()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {BehaviorSubject, from} from 'rxjs'; 2 | import {Injectable, OnDestroy} from '@angular/core'; 3 | import {HttpClient} from '@angular/common/http'; 4 | import {Plugins} from '@capacitor/core'; 5 | import {map, tap} from 'rxjs/operators'; 6 | 7 | import {environment} from '../../environments/environment'; 8 | import {User} from './user.model'; 9 | 10 | export interface AuthResponseData { 11 | kind: string; 12 | idToken: string; 13 | email: string; 14 | refreshToken: string; 15 | localId: string; 16 | expiresIn: string; 17 | registered?: boolean; 18 | } 19 | 20 | @Injectable({ 21 | providedIn: 'root' 22 | }) 23 | export class AuthService implements OnDestroy { 24 | private _user = new BehaviorSubject(null); 25 | private activeLogoutTimer: any; 26 | 27 | // method to return true if user has auth token, false if not 28 | get userIsAuthenticated() { 29 | return this._user.asObservable().pipe( 30 | map(user => { 31 | if (user) { 32 | return !!user.token; 33 | } else { 34 | return false; 35 | } 36 | }) 37 | ); 38 | } 39 | 40 | // function to return userid as an observable 41 | get userId() { 42 | return this._user.asObservable().pipe( 43 | map(user => { 44 | if (user) { 45 | return user.id; 46 | } else { 47 | return null; 48 | } 49 | }) 50 | ); 51 | } 52 | 53 | get token() { 54 | return this._user.asObservable().pipe( 55 | map(user => { 56 | if (user) { 57 | return user.token; 58 | } else { 59 | return null; 60 | } 61 | }) 62 | ); 63 | } 64 | 65 | constructor(private http: HttpClient) {} 66 | 67 | // method to get data stored by Capacitor plugin then convert it back to a js object 68 | autoLogin() { 69 | return from(Plugins.Storage.get({key: 'authData'})).pipe( 70 | map(storedData => { 71 | if (!storedData || !storedData.value) { 72 | return null; 73 | } 74 | // create a constant that is a javascript object 75 | const parsedData = JSON.parse(storedData.value) as { 76 | token: string 77 | tokenExpirationDate: string 78 | userId: string 79 | email: string 80 | }; 81 | // recreate expiry time as a string in ISO format that can be used by the data constructor 82 | const expirationTime = new Date(parsedData.tokenExpirationDate); 83 | if (expirationTime <= new Date()) { 84 | return null; 85 | } 86 | const user = new User(parsedData.userId, parsedData.email, parsedData.token, expirationTime); 87 | return user; 88 | }), 89 | tap(user => { 90 | if (user) { 91 | this._user.next(user); 92 | this.autoLogout(user.tokenDuration); 93 | } 94 | }), 95 | map(user => { 96 | return !!user; // returns a boolean (true if there is a user, otherwise false) 97 | }) 98 | ); 99 | } 100 | 101 | // signup POST request method with a js object with user login info to a firebase backend http endpoint with an API key. 102 | // This will return js object AuthResponseData (kind, idToken, email, refreshToken, localId, expiresIn). 103 | // return observable to auth page. Bind the data returned from firebase to setUserData. 104 | signup(email: string, password: string) { 105 | return this.http 106 | .post( 107 | `https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${ 108 | environment.firebaseAPIKey 109 | }`, 110 | {email, password, returnSecureToken: true}) 111 | .pipe(tap(this.setUserData.bind(this))); 112 | } 113 | 114 | // will return AuthResponseData (kind, idToken, email, refreshToken, localId, expiresIn, registered boolean) 115 | // return observable to auth page. Bind the data returned from firebase to setUserData. 116 | login(email: string, password: string) { 117 | return this.http 118 | .post( 119 | `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${ 120 | environment.firebaseAPIKey 121 | }`, 122 | {email, password, returnSecureToken: true}) 123 | .pipe(tap(this.setUserData.bind(this))); 124 | } 125 | 126 | // method to logout user by clearing timer, passing null to the _user behavioursubject and clearing the native storage. 127 | logout() { 128 | if (this.activeLogoutTimer) { 129 | clearTimeout(this.activeLogoutTimer); 130 | } 131 | this._user.next(null); 132 | Plugins.Storage.remove({key: 'authData'}); 133 | } 134 | 135 | ngOnDestroy() { 136 | if (this.activeLogoutTimer) { 137 | clearTimeout(this.activeLogoutTimer); 138 | } 139 | } 140 | 141 | private autoLogout(duration: number) { 142 | if (this.activeLogoutTimer) { 143 | clearTimeout(this.activeLogoutTimer); 144 | } 145 | this.activeLogoutTimer = setTimeout(() => { 146 | this.logout(); 147 | }, duration); 148 | } 149 | 150 | private setUserData(userData: AuthResponseData) { 151 | // create const for time to expire in milliseconds 152 | const expirationTime = new Date(new Date().getTime() + +userData.expiresIn * 1000); 153 | const user = new User(userData.localId, userData.email, userData.idToken, expirationTime); 154 | this._user.next(user); 155 | this.autoLogout(user.tokenDuration); 156 | this.storeAuthData(userData.localId, userData.idToken, expirationTime.toISOString(), userData.email); 157 | } 158 | 159 | // method to store auth data using Capacitor storage plugin 160 | // convert to string using stringify as we cannot store a js object 161 | private storeAuthData(userId: string, token: string, tokenExpirationDate: string, email: string) { 162 | const data = JSON.stringify({ 163 | userId, 164 | token, 165 | tokenExpirationDate, 166 | email 167 | }); 168 | Plugins.Storage.set({key: 'authData', value: data}); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/app/auth/user.model.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | constructor( 3 | public id: string, 4 | public email: string, 5 | // tslint:disable-next-line: variable-name 6 | private _token: string, 7 | private tokenExpirationDate: Date 8 | ) {} 9 | 10 | get token() { 11 | if (!this.tokenExpirationDate || this.tokenExpirationDate <= new Date()) { 12 | return null; 13 | } 14 | return this._token; 15 | } 16 | 17 | get tokenDuration() { 18 | if (!this.token) { 19 | return 0; 20 | } 21 | return this.tokenExpirationDate.getTime() - new Date().getTime(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/bookings/booking.model.ts: -------------------------------------------------------------------------------- 1 | export class Booking { 2 | constructor( 3 | public id: string, 4 | public placeId: string, 5 | public userId: string, 6 | public placeTitle: string, 7 | public placeImage: string, 8 | public firstName: string, 9 | public lastName: string, 10 | public guestNumber: number, 11 | public bookedFrom: Date, 12 | public bookedTo: Date 13 | ) {} 14 | } 15 | -------------------------------------------------------------------------------- /src/app/bookings/booking.service.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs'; 2 | import { Injectable } from '@angular/core'; 3 | import { take, tap, switchMap, map } from 'rxjs/operators'; 4 | import { HttpClient } from '@angular/common/http'; 5 | 6 | import { AuthService } from '../auth/auth.service'; 7 | import { Booking } from './booking.model'; 8 | 9 | interface BookingData { 10 | bookedFrom: string; 11 | bookedTo: string; 12 | firstName: string; 13 | lastName: string; 14 | guestNumber: number; 15 | placeId: string; 16 | placeImage: string; 17 | placeTitle: string; 18 | userId: string; 19 | } 20 | 21 | @Injectable({ providedIn: 'root' }) 22 | export class BookingService { 23 | private _bookings = new BehaviorSubject([]); 24 | 25 | get bookings() { 26 | return this._bookings.asObservable(); 27 | } 28 | 29 | constructor(private authService: AuthService, private http: HttpClient) {} 30 | 31 | addBooking( 32 | placeId: string, 33 | placeTitle: string, 34 | placeImage: string, 35 | firstName: string, 36 | lastName: string, 37 | guestNumber: number, 38 | dateFrom: Date, 39 | dateTo: Date 40 | ) { 41 | let generatedId: string; 42 | let newBooking: Booking; 43 | let fetchedUserId: string; 44 | return this.authService.userId.pipe( 45 | take(1), 46 | switchMap(userId => { 47 | if (!userId) { 48 | throw new Error('no user id found'); 49 | } 50 | fetchedUserId = userId; 51 | return this.authService.token; 52 | }), 53 | take(1), 54 | switchMap(token => { 55 | newBooking = new Booking( 56 | Math.random().toString(), 57 | placeId, 58 | fetchedUserId, 59 | placeTitle, 60 | placeImage, 61 | firstName, 62 | lastName, 63 | guestNumber, 64 | dateFrom, 65 | dateTo 66 | ); 67 | return this.http 68 | .post<{ name: string }>( 69 | `https://ionic-maps-api-1565705298126.firebaseio.com/bookings.json?auth=${token}`, 70 | { ...newBooking, id: null } 71 | ); 72 | }), 73 | switchMap(resData => { 74 | generatedId = resData.name; 75 | return this.bookings; 76 | }), 77 | take(1), 78 | tap(bookings => { 79 | newBooking.id = generatedId; 80 | this._bookings.next(bookings.concat(newBooking)); 81 | }) 82 | ); 83 | } 84 | 85 | cancelBooking(bookingId: string) { 86 | return this.authService.token.pipe( 87 | take(1), 88 | switchMap(token => { 89 | return this.http 90 | .delete( 91 | `https://ionic-maps-api-1565705298126.firebaseio.com/bookings/${bookingId}.json?auth=${token}` 92 | ); 93 | }), 94 | switchMap(() => { 95 | return this.bookings; 96 | }), 97 | take(1), 98 | tap(bookings => { 99 | this._bookings.next(bookings.filter(b => b.id !== bookingId)); 100 | }) 101 | ); 102 | } 103 | 104 | fetchBookings() { 105 | let fetchedUserId: string; 106 | return this.authService.userId.pipe( 107 | take(1), 108 | switchMap(userId => { 109 | if (!userId) { 110 | throw new Error('User not found'); 111 | } 112 | fetchedUserId = userId; 113 | return this.authService.token; 114 | }), 115 | take(1), 116 | switchMap(token => { 117 | return this.http.get<{ [key: string]: BookingData }>( 118 | `https://ionic-maps-api-1565705298126.firebaseio.com/bookings.json?orderBy="userId"&equalTo="${fetchedUserId}"&auth=${token}` 119 | ); 120 | }), 121 | map(bookingData => { 122 | const bookings = []; 123 | for (const key in bookingData) { 124 | if (bookingData.hasOwnProperty(key)) { 125 | bookings.push( 126 | new Booking( 127 | key, 128 | bookingData[key].placeId, 129 | bookingData[key].userId, 130 | bookingData[key].placeTitle, 131 | bookingData[key].placeImage, 132 | bookingData[key].firstName, 133 | bookingData[key].lastName, 134 | bookingData[key].guestNumber, 135 | new Date(bookingData[key].bookedFrom), 136 | new Date(bookingData[key].bookedTo) 137 | ) 138 | ); 139 | } 140 | } 141 | return bookings; 142 | }), 143 | tap(bookings => { 144 | this._bookings.next(bookings); 145 | }) 146 | ); 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/app/bookings/bookings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { BookingsPage } from './bookings.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: BookingsPage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [BookingsPage] 25 | }) 26 | export class BookingsPageModule {} 27 | -------------------------------------------------------------------------------- /src/app/bookings/bookings.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Your Bookings 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 |

No bookings found

23 |
24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
{{ booking.placeTitle }}
38 |

Guests: {{ booking.guestNumber }}

39 |
40 |
41 | 42 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 | -------------------------------------------------------------------------------- /src/app/bookings/bookings.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/bookings/bookings.page.scss -------------------------------------------------------------------------------- /src/app/bookings/bookings.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { BookingsPage } from './bookings.page'; 5 | 6 | describe('BookingsPage', () => { 7 | let component: BookingsPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ BookingsPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(BookingsPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/bookings/bookings.page.ts: -------------------------------------------------------------------------------- 1 | import { IonItemSliding, LoadingController } from '@ionic/angular'; 2 | import { Component, OnInit, Input, OnDestroy } from '@angular/core'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { BookingService } from './booking.service'; 6 | import { Booking } from './booking.model'; 7 | 8 | @Component({ 9 | selector: 'app-bookings', 10 | templateUrl: './bookings.page.html', 11 | styleUrls: ['./bookings.page.scss'], 12 | }) 13 | export class BookingsPage implements OnInit, OnDestroy { 14 | loadedBookings: Booking[]; 15 | isLoading = false; 16 | private bookingSub: Subscription; 17 | 18 | constructor( 19 | private bookingService: BookingService, 20 | private loadingCtrl: LoadingController 21 | ) { } 22 | 23 | ngOnInit() { 24 | this.bookingSub = this.bookingService.bookings.subscribe(bookings => { 25 | this.loadedBookings = bookings; 26 | }); 27 | } 28 | 29 | // lifecycle event fired when entering a page 30 | ionViewWillEnter() { 31 | this.isLoading = true; 32 | this.bookingService.fetchBookings().subscribe(() => { 33 | this.isLoading = false; 34 | }); 35 | } 36 | 37 | onCancelBooking(bookingId: string, slidingEl: IonItemSliding) { 38 | slidingEl.close(); 39 | this.loadingCtrl.create({message: 'Cancelling...'}).then(loadingEl => { 40 | loadingEl.present(); 41 | this.bookingService.cancelBooking(bookingId).subscribe(() => { 42 | loadingEl.dismiss(); 43 | }); 44 | }); 45 | } 46 | 47 | ngOnDestroy() { 48 | if (this.bookingSub) { 49 | this.bookingSub.unsubscribe(); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/app/bookings/create-booking/create-booking.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ selectedPlace.title }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | First Name 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Last Name 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Number of Guests 51 | 52 | 1 53 | 2 54 | 3 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | From 65 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | To 84 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 104 | Book 105 | 106 | 107 | 108 | 109 | 110 |
111 |
-------------------------------------------------------------------------------- /src/app/bookings/create-booking/create-booking.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/bookings/create-booking/create-booking.component.scss -------------------------------------------------------------------------------- /src/app/bookings/create-booking/create-booking.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { CreateBookingComponent } from './create-booking.component'; 5 | 6 | describe('CreateBookingComponent', () => { 7 | let component: CreateBookingComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ CreateBookingComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(CreateBookingComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/bookings/create-booking/create-booking.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ViewChild } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { NgForm } from '@angular/forms'; 4 | 5 | import { Place } from '../../places/place.model'; 6 | 7 | @Component({ 8 | selector: 'app-create-booking', 9 | templateUrl: './create-booking.component.html', 10 | styleUrls: ['./create-booking.component.scss'], 11 | }) 12 | export class CreateBookingComponent implements OnInit { 13 | @Input() selectedPlace: Place; 14 | @Input() selectedMode: 'select' | 'random'; 15 | @ViewChild('f') form: NgForm; 16 | startDate: string; 17 | endDate: string; 18 | 19 | constructor(private modalCtrl: ModalController) { } 20 | 21 | ngOnInit() { 22 | const availableFrom = new Date(this.selectedPlace.availableFrom); 23 | const availableTo = new Date(this.selectedPlace.availableTo); 24 | if (this.selectedMode === 'random') { 25 | this.startDate = new Date( 26 | availableFrom.getTime() + 27 | Math.random() * 28 | (availableTo.getTime() - 29 | 7 * 24 * 60 * 60 * 1000 - 30 | availableFrom.getTime()) 31 | ).toISOString(); 32 | 33 | this.endDate = new Date( 34 | new Date(this.startDate).getTime() + 35 | Math.random() * 36 | (new Date(this.startDate).getTime() + 37 | 6 * 24 * 60 * 60 * 1000 - 38 | new Date(this.startDate).getTime()) 39 | ).toISOString(); 40 | } 41 | } 42 | 43 | onCancel() { 44 | this.modalCtrl.dismiss(null, 'cancel'); 45 | } 46 | 47 | onBookPlace() { 48 | if (!this.form.valid || !this.datesValid) { 49 | return; 50 | } 51 | 52 | this.modalCtrl.dismiss( 53 | { 54 | bookingData: { 55 | firstName: this.form.value['first-name'], 56 | lastName: this.form.value['last-name'], 57 | guestNumber: +this.form.value['guest-number'], 58 | startDate: new Date(this.form.value['date-from']), 59 | endDate: new Date(this.form.value['date-to']) 60 | } 61 | }, 62 | 'confirm' 63 | ); 64 | } 65 | 66 | datesValid() { 67 | const startDate = new Date(this.form.value['date-from']); 68 | const endDate = new Date(this.form.value['date-to']); 69 | return endDate > startDate; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/places/discover/discover.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { DiscoverPage } from './discover.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: DiscoverPage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [DiscoverPage] 25 | }) 26 | export class DiscoverPageModule {} 27 | -------------------------------------------------------------------------------- /src/app/places/discover/discover.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Discover Places 7 | 8 | 9 | 10 | 11 | 12 | All Places 13 | Bookable Places 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |

There are no bookable places

28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | {{ relevantPlaces[0].title }} 38 | {{ relevantPlaces[0].price | currency }} / Night 39 | 40 | 41 | 42 |

{{ relevantPlaces[0].description}}

43 |
44 |
45 | 56 | View 57 | 58 |
59 |
60 |
61 |
62 | 63 | 64 | 65 | 76 | 77 | 78 | 79 | 80 |

{{ place.title }}

81 |

{{ place.description }}

82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /src/app/places/discover/discover.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/places/discover/discover.page.scss -------------------------------------------------------------------------------- /src/app/places/discover/discover.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { DiscoverPage } from './discover.page'; 5 | 6 | describe('DiscoverPage', () => { 7 | let component: DiscoverPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ DiscoverPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(DiscoverPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/places/discover/discover.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { MenuController } from '@ionic/angular'; 3 | import { SegmentChangeEventDetail } from '@ionic/core'; 4 | import { Subscription } from 'rxjs'; 5 | import { take } from 'rxjs/operators'; 6 | 7 | import { PlacesService } from '../places.service'; 8 | import { Place } from '../place.model'; 9 | import { AuthService } from 'src/app/auth/auth.service'; 10 | 11 | @Component({ 12 | selector: 'app-discover', 13 | templateUrl: './discover.page.html', 14 | styleUrls: ['./discover.page.scss'], 15 | }) 16 | export class DiscoverPage implements OnInit, OnDestroy { 17 | loadedPlaces: Place[]; 18 | listedLoadedPlaces: Place[]; 19 | relevantPlaces: Place[]; 20 | isLoading = false; 21 | private placesSub: Subscription; 22 | 23 | constructor( 24 | private placesService: PlacesService, 25 | private menuCtrl: MenuController, 26 | private authService: AuthService 27 | ) { } 28 | 29 | ngOnInit() { 30 | this.placesSub = this.placesService.places.subscribe(places => { 31 | this.loadedPlaces = places; 32 | this.relevantPlaces = this.loadedPlaces; 33 | this.listedLoadedPlaces = this.relevantPlaces.slice(1); 34 | }); 35 | } 36 | 37 | ionViewWillEnter() { 38 | this.isLoading = true; 39 | this.placesService.fetchPlaces().subscribe(() => { 40 | this.isLoading = false; 41 | }); 42 | } 43 | 44 | onOpenMenu() { 45 | this.menuCtrl.toggle(); 46 | } 47 | 48 | onFilterUpdate(event: CustomEvent) { 49 | this.authService.userId.pipe(take(1)).subscribe(userId => { 50 | if (event.detail.value === 'all') { 51 | this.relevantPlaces = this.loadedPlaces; 52 | this.listedLoadedPlaces = this.relevantPlaces.slice(1); 53 | } else { 54 | this.relevantPlaces = this.loadedPlaces.filter( 55 | place => place.userId !== userId 56 | ); 57 | this.listedLoadedPlaces = this.relevantPlaces.slice(1); 58 | } 59 | }); 60 | } 61 | 62 | ngOnDestroy() { 63 | if (this.placesSub) { 64 | this.placesSub.unsubscribe(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/places/discover/place-detail/place-detail.module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { Routes, RouterModule } from '@angular/router'; 6 | 7 | import { IonicModule } from '@ionic/angular'; 8 | 9 | import { PlaceDetailPage } from './place-detail.page'; 10 | import { CreateBookingComponent } from './../../../bookings/create-booking/create-booking.component'; 11 | import { SharedModule } from '../../../shared/shared.module'; 12 | 13 | const routes: Routes = [ 14 | { 15 | path: '', 16 | component: PlaceDetailPage 17 | } 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [ 22 | CommonModule, 23 | FormsModule, 24 | IonicModule, 25 | RouterModule.forChild(routes), 26 | SharedModule 27 | ], 28 | declarations: [PlaceDetailPage, CreateBookingComponent] 29 | }) 30 | export class PlaceDetailPageModule {} 31 | -------------------------------------------------------------------------------- /src/app/places/discover/place-detail/place-detail.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ isLoading? 'Loading...' : place.title }} 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

{{ place.description }}

25 |
26 |
27 | 28 | 29 | 30 |

{{ place.location.address }}

31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Book 43 | 44 | 45 |
46 |
47 | -------------------------------------------------------------------------------- /src/app/places/discover/place-detail/place-detail.page.scss: -------------------------------------------------------------------------------- 1 | .location-image { 2 | width: 100%; 3 | height: 100%; 4 | max-height: 30vh; 5 | object-fit: cover; 6 | } 7 | 8 | p { 9 | margin: 0; 10 | } -------------------------------------------------------------------------------- /src/app/places/discover/place-detail/place-detail.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { PlaceDetailPage } from './place-detail.page'; 5 | 6 | describe('PlaceDetailPage', () => { 7 | let component: PlaceDetailPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ PlaceDetailPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(PlaceDetailPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/places/discover/place-detail/place-detail.page.ts: -------------------------------------------------------------------------------- 1 | import { ActivatedRoute, Router } from '@angular/router'; 2 | import { Component, OnInit, OnDestroy } from '@angular/core'; 3 | import { NavController, ModalController, ActionSheetController, LoadingController, AlertController } from '@ionic/angular'; 4 | import { Subscription } from 'rxjs'; 5 | import { switchMap, take } from 'rxjs/operators'; 6 | 7 | 8 | import { PlacesService } from '../../places.service'; 9 | import { Place } from '../../place.model'; 10 | import { CreateBookingComponent } from '../../../bookings/create-booking/create-booking.component'; 11 | import { AuthService } from '../../../auth/auth.service'; 12 | import { BookingService } from '../../../bookings/booking.service'; 13 | import { MapModalComponent } from '../../../shared/map-modal/map-modal.component'; 14 | 15 | @Component({ 16 | selector: 'app-place-detail', 17 | templateUrl: './place-detail.page.html', 18 | styleUrls: ['./place-detail.page.scss'], 19 | }) 20 | export class PlaceDetailPage implements OnInit, OnDestroy { 21 | place: Place; 22 | isBookable = false; 23 | isLoading = false; 24 | private placeSub: Subscription; 25 | 26 | constructor( 27 | private navCtrl: NavController, 28 | private route: ActivatedRoute, 29 | private placesService: PlacesService, 30 | private modalCtrl: ModalController, 31 | private actionSheetCtrl: ActionSheetController, 32 | private bookingService: BookingService, 33 | private loadingCtrl: LoadingController, 34 | private authService: AuthService, 35 | private alertCtrl: AlertController, 36 | private router: Router 37 | ) {} 38 | 39 | ngOnInit() { 40 | // paramMap manages it's own subscription 41 | this.route.paramMap.subscribe(paramMap => { 42 | if (!paramMap.has('placeId')) { 43 | this.navCtrl.navigateBack('/places/tabs/offers'); 44 | return; 45 | } 46 | this.isLoading = true; 47 | let fetchedUserId: string; 48 | this.authService.userId 49 | .pipe( 50 | take(1), 51 | switchMap( 52 | userId => { 53 | if (!userId) { 54 | throw new Error('found no user'); 55 | } 56 | fetchedUserId = userId; 57 | return this.placesService.getPlace(paramMap.get('placeId')); 58 | }) 59 | ) 60 | .subscribe(place => { 61 | this.place = place; 62 | this.isBookable = place.userId !== fetchedUserId; 63 | this.isLoading = false; 64 | }, 65 | error => { 66 | this.alertCtrl.create({ 67 | header: 'An error occured', 68 | message: 'Could not load place.', 69 | buttons: [ 70 | { 71 | text: 'ok', 72 | handler: () => { 73 | this.router.navigate(['/places/tabs/discover']); 74 | } 75 | } 76 | ] 77 | }) 78 | .then(alertEl => alertEl.present()); 79 | }); 80 | }); 81 | } 82 | onBookPlace() { 83 | // this.navCtrl.navigateBack('/places/tabs/discover'); 84 | this.actionSheetCtrl.create({ 85 | header: 'Choose an action', 86 | buttons: [ 87 | { 88 | text: 'Select Date', 89 | handler: () => { 90 | this.openBookingModal('select'); 91 | } 92 | }, 93 | { 94 | text: 'Random Date', 95 | handler: () => { 96 | this.openBookingModal('random'); 97 | } 98 | }, 99 | { 100 | text: 'Cancel', 101 | role: 'cancel' 102 | } 103 | ] 104 | }) 105 | .then(actionSheetEl => { 106 | actionSheetEl.present(); 107 | }); 108 | } 109 | 110 | openBookingModal(mode: 'select' | 'random') { 111 | console.log(mode); 112 | this.modalCtrl 113 | .create({ 114 | component: CreateBookingComponent, 115 | componentProps: { selectedPlace: this.place, selectedMode: mode } 116 | }) 117 | .then(modalEl => { 118 | modalEl.present(); 119 | return modalEl.onDidDismiss(); 120 | }) 121 | .then(resultData => { 122 | if (resultData.role === 'confirm') { 123 | this.loadingCtrl 124 | .create({message: 'Booking place...'}) 125 | .then(loadingEl => { 126 | loadingEl.present(); 127 | const data = resultData.data.bookingData; 128 | this.bookingService 129 | .addBooking( 130 | this.place.id, 131 | this.place.title, 132 | this.place.imageUrl, 133 | data.firstName, 134 | data.lastName, 135 | data.guestNumber, 136 | data.startDate, 137 | data.endDate 138 | ) 139 | .subscribe(() => { 140 | loadingEl.dismiss(); 141 | }); 142 | }); 143 | } 144 | }); 145 | } 146 | 147 | onShowFullMap() { 148 | this.modalCtrl 149 | .create({ 150 | component: MapModalComponent, 151 | componentProps: { 152 | center: { 153 | lat: this.place.location.lat, 154 | lng: this.place.location.lng 155 | }, 156 | selectable: false, 157 | closeButtonText: 'Close', 158 | title: this.place.location.address 159 | } 160 | }) 161 | .then(modalEl => { 162 | modalEl.present(); 163 | }); 164 | } 165 | 166 | ngOnDestroy() { 167 | if (this.placeSub) { 168 | this.placeSub.unsubscribe(); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/app/places/location.model.ts: -------------------------------------------------------------------------------- 1 | export interface Coordinates { 2 | lat: number; 3 | lng: number; 4 | } 5 | 6 | export interface PlaceLocation extends Coordinates { 7 | address: string; 8 | staticMapImageUrl: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/places/offers/edit-offer/edit-offer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { EditOfferPage } from './edit-offer.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: EditOfferPage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | ReactiveFormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [EditOfferPage] 25 | }) 26 | export class EditOfferPageModule {} 27 | -------------------------------------------------------------------------------- /src/app/places/offers/edit-offer/edit-offer.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | Edit Offer 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | Title 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Short Description 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |

Description must be between 1 and 180 characters.

54 |
55 |
56 | 57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /src/app/places/offers/edit-offer/edit-offer.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/places/offers/edit-offer/edit-offer.page.scss -------------------------------------------------------------------------------- /src/app/places/offers/edit-offer/edit-offer.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { EditOfferPage } from './edit-offer.page'; 5 | 6 | describe('EditOfferPage', () => { 7 | let component: EditOfferPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ EditOfferPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(EditOfferPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/places/offers/edit-offer/edit-offer.page.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 2 | import { NavController, LoadingController, AlertController } from '@ionic/angular'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { Component, OnInit, OnDestroy } from '@angular/core'; 5 | import { Subscription } from 'rxjs'; 6 | 7 | import { PlacesService } from '../../places.service'; 8 | import { Place } from '../../place.model'; 9 | 10 | 11 | @Component({ 12 | selector: 'app-edit-offer', 13 | templateUrl: './edit-offer.page.html', 14 | styleUrls: ['./edit-offer.page.scss'], 15 | }) 16 | export class EditOfferPage implements OnInit, OnDestroy { 17 | place: Place; 18 | placeId: string; 19 | form: FormGroup; 20 | isLoading = false; 21 | private placeSub: Subscription; 22 | 23 | constructor( 24 | private route: ActivatedRoute, 25 | private placesService: PlacesService, 26 | private navCtrl: NavController, 27 | private router: Router, 28 | private loadingCtrl: LoadingController, 29 | private alertCtrl: AlertController 30 | ) { } 31 | 32 | ngOnInit() { 33 | this.route.paramMap.subscribe(paramMap => { 34 | if (!paramMap.has('placeId')) { 35 | this.navCtrl.navigateBack('/places/tabs/offers'); 36 | return; 37 | } 38 | this.placeId = paramMap.get('placeId'); 39 | this.isLoading = true; 40 | this.placeSub = this.placesService 41 | .getPlace(paramMap.get('placeId')) 42 | .subscribe(place => { 43 | this.place = place; 44 | this.form = new FormGroup({ 45 | title: new FormControl(this.place.title, { 46 | updateOn: 'blur', 47 | validators: [Validators.required] 48 | }), 49 | description: new FormControl(this.place.description, { 50 | updateOn: 'blur', 51 | validators: [Validators.required, Validators.maxLength(180)] 52 | }) 53 | }); 54 | this.isLoading = false; 55 | }, error => { 56 | this.alertCtrl.create({ 57 | header: 'An error occured', 58 | message: 'Place could not be fetched. Try again later.', 59 | buttons: [{text: 'ok', handler: () => { 60 | this.router.navigate(['/places/tabs/offers']); 61 | }}] 62 | }).then(alertEl => { 63 | alertEl.present(); 64 | }); 65 | }); 66 | }); 67 | } 68 | 69 | onUpdateOffer() { 70 | if (!this.form.valid) { 71 | return; 72 | } 73 | this.loadingCtrl.create({ 74 | message: 'Updating place...' 75 | }) 76 | .then(loadingEl => { 77 | loadingEl.present(); 78 | this.placesService.updatePlace( 79 | this.place.id, 80 | this.form.value.title, 81 | this.form.value.description 82 | ).subscribe(() => { 83 | loadingEl.dismiss(); 84 | this.form.reset(); 85 | this.router.navigate(['/places/tabs/offers']); 86 | }); 87 | }); 88 | 89 | } 90 | 91 | ngOnDestroy() { 92 | if (this.placeSub) { 93 | this.placeSub.unsubscribe(); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/app/places/offers/new-offer/base64ToBlob.utility.ts: -------------------------------------------------------------------------------- 1 | export function base64toBlob(base64Data, contentType) { 2 | contentType = contentType || ''; 3 | const sliceSize = 1024; 4 | const byteCharacters = window.atob(base64Data); 5 | const bytesLength = byteCharacters.length; 6 | const slicesCount = Math.ceil(bytesLength / sliceSize); 7 | const byteArrays = new Array(slicesCount); 8 | 9 | for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) { 10 | const begin = sliceIndex * sliceSize; 11 | const end = Math.min(begin + sliceSize, bytesLength); 12 | 13 | const bytes = new Array(end - begin); 14 | for (let offset = begin, i = 0; offset < end; ++i, ++offset) { 15 | bytes[i] = byteCharacters[offset].charCodeAt(0); 16 | } 17 | byteArrays[sliceIndex] = new Uint8Array(bytes); 18 | } 19 | return new Blob(byteArrays, { type: contentType }); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/places/offers/new-offer/new-offer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { Routes, RouterModule } from '@angular/router'; 4 | 5 | import { IonicModule } from '@ionic/angular'; 6 | 7 | import { NewOfferPage } from './new-offer.page'; 8 | import { ReactiveFormsModule } from '@angular/forms'; 9 | import { SharedModule } from '../../../shared/shared.module'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | component: NewOfferPage 15 | } 16 | ]; 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | IonicModule, 21 | ReactiveFormsModule, 22 | RouterModule.forChild(routes), 23 | SharedModule 24 | ], 25 | declarations: [NewOfferPage] 26 | }) 27 | export class NewOfferPageModule {} 28 | -------------------------------------------------------------------------------- /src/app/places/offers/new-offer/new-offer.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | New Offer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | Title 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Short Description 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |

Description must be between 1 and 180 characters.

43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | Price 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Available from 61 | 62 | 63 | 64 | 65 | 66 | Available to 67 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 90 | 91 | 92 | 93 | 94 |
95 |
96 |
97 | -------------------------------------------------------------------------------- /src/app/places/offers/new-offer/new-offer.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/places/offers/new-offer/new-offer.page.scss -------------------------------------------------------------------------------- /src/app/places/offers/new-offer/new-offer.page.ts: -------------------------------------------------------------------------------- 1 | import { LoadingController } from '@ionic/angular'; 2 | import { Router } from '@angular/router'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 5 | import { switchMap } from 'rxjs/operators'; 6 | 7 | import { PlaceLocation } from './../../location.model'; 8 | import { PlacesService } from '../../places.service'; 9 | import { base64toBlob } from './base64ToBlob.utility'; 10 | 11 | @Component({ 12 | selector: 'app-new-offer', 13 | templateUrl: './new-offer.page.html', 14 | styleUrls: ['./new-offer.page.scss'], 15 | }) 16 | export class NewOfferPage implements OnInit { 17 | form: FormGroup; 18 | 19 | constructor( 20 | private placesService: PlacesService, 21 | private router: Router, 22 | private loadingCtrl: LoadingController 23 | ) { } 24 | 25 | ngOnInit() { 26 | this.form = new FormGroup({ 27 | title: new FormControl(null, { 28 | updateOn: 'blur', 29 | validators: [Validators.required] 30 | }), 31 | description: new FormControl(null, { 32 | updateOn: 'blur', 33 | validators: [Validators.required, Validators.maxLength(180)] 34 | }), 35 | price: new FormControl(null, { 36 | updateOn: 'blur', 37 | validators: [Validators.required, Validators.min(1)] 38 | }), 39 | dateFrom: new FormControl(null, { 40 | updateOn: 'blur', 41 | validators: [Validators.required] 42 | }), 43 | dateTo: new FormControl(null, { 44 | updateOn: 'blur', 45 | validators: [Validators.required] 46 | }), 47 | location: new FormControl(null, { validators: [Validators.required]}), 48 | image: new FormControl(null) 49 | }); 50 | } 51 | 52 | onLocationPicked(location: PlaceLocation) { 53 | this.form.patchValue({location}); 54 | } 55 | 56 | onImagePicked(imageData: string | File) { 57 | let imageFile; 58 | if (typeof imageData === 'string') { 59 | try { 60 | imageFile = base64toBlob( 61 | imageData 62 | .replace('data:image/jpeg;base64,', ''), 63 | 'image/jpeg' 64 | ); 65 | } catch (error) { 66 | console.log(error); 67 | return; 68 | } 69 | } else { 70 | imageFile = imageData; 71 | } 72 | this.form.patchValue({ image: imageFile }); 73 | } 74 | 75 | onCreateOffer() { 76 | if (!this.form.valid || !this.form.get('image').value) { 77 | return; 78 | } 79 | console.log(this.form.value); 80 | this.loadingCtrl 81 | .create({ 82 | message: 'Creating place...' 83 | }) 84 | .then(loadingEl => { 85 | loadingEl.present(); 86 | this.placesService 87 | .uploadImage(this.form.get('image').value) 88 | .pipe( 89 | switchMap(uploadRes => { 90 | return this.placesService.addPlace( 91 | this.form.value.title, 92 | this.form.value.description, 93 | +this.form.value.price, 94 | new Date(this.form.value.dateFrom), 95 | new Date(this.form.value.dateTo), 96 | this.form.value.location, 97 | uploadRes.imageUrl 98 | ); 99 | }) 100 | ) 101 | .subscribe(() => { 102 | loadingEl.dismiss(); 103 | this.form.reset(); 104 | this.router.navigate(['/places/tabs/offers']); 105 | }); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/places/offers/offer-item/offer-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

{{ offer.title }}

7 |
8 | 9 | {{ 10 | offer.availableFrom | date 11 | }} 12 | to 13 | 14 | {{ 15 | offer.availableTo | date 16 | }} 17 |
18 |
19 |
-------------------------------------------------------------------------------- /src/app/places/offers/offer-item/offer-item.component.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 18px; 3 | border: 1px solid var(--ion-color-primary); 4 | padding: 8px; 5 | border-radius: 5px; 6 | color: var(--ion-color-primary); 7 | } 8 | 9 | .offer-details { 10 | display: flex; 11 | align-items: center; 12 | 13 | .space-left { 14 | margin-left: 5px; 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/places/offers/offer-item/offer-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { OfferItemComponent } from './offer-item.component'; 5 | 6 | describe('OfferItemComponent', () => { 7 | let component: OfferItemComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ OfferItemComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(OfferItemComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/places/offers/offer-item/offer-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { Place } from '../../place.model'; 3 | 4 | @Component({ 5 | selector: 'app-offer-item', 6 | templateUrl: './offer-item.component.html', 7 | styleUrls: ['./offer-item.component.scss'], 8 | }) 9 | export class OfferItemComponent implements OnInit { 10 | @Input() offer: Place; 11 | 12 | constructor() { } 13 | 14 | ngOnInit() {} 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/places/offers/offers.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { OffersPage } from './offers.page'; 9 | import { OfferItemComponent } from './offer-item/offer-item.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | component: OffersPage 15 | } 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [ 20 | CommonModule, 21 | FormsModule, 22 | IonicModule, 23 | RouterModule.forChild(routes) 24 | ], 25 | declarations: [OffersPage, OfferItemComponent] 26 | }) 27 | export class OffersPageModule {} 28 | -------------------------------------------------------------------------------- /src/app/places/offers/offers.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Offers 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 |

No offers found. You need to create one.

25 | Offer New Place 26 |
27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/app/places/offers/offers.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/places/offers/offers.page.scss -------------------------------------------------------------------------------- /src/app/places/offers/offers.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { OffersPage } from './offers.page'; 5 | 6 | describe('OffersPage', () => { 7 | let component: OffersPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ OffersPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(OffersPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/places/offers/offers.page.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { Component, OnInit, OnDestroy } from '@angular/core'; 3 | import { IonItemSliding } from '@ionic/angular'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | import { PlacesService } from './../places.service'; 7 | import { Place } from '../place.model'; 8 | 9 | 10 | @Component({ 11 | selector: 'app-offers', 12 | templateUrl: './offers.page.html', 13 | styleUrls: ['./offers.page.scss'], 14 | }) 15 | export class OffersPage implements OnInit, OnDestroy { 16 | offers: Place[]; 17 | isLoading = false; 18 | private placesSub: Subscription; 19 | 20 | constructor(private placesService: PlacesService, private router: Router) { } 21 | 22 | ngOnInit() { 23 | this.placesSub = this.placesService.places.subscribe(places => { 24 | this.offers = places; 25 | }); 26 | } 27 | 28 | ionViewWillEnter() { 29 | this.placesService.fetchPlaces().subscribe(); 30 | } 31 | 32 | onEdit(offerId: string, slidingItem: IonItemSliding) { 33 | slidingItem.close(); 34 | this.router.navigate(['/', 'places', 'tabs', 'offers', 'edit', offerId]); 35 | console.log('Editing item', offerId); 36 | } 37 | 38 | ngOnDestroy() { 39 | if (this.placesSub) { 40 | this.placesSub.unsubscribe(); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/app/places/place.model.ts: -------------------------------------------------------------------------------- 1 | import { PlaceLocation } from './location.model'; 2 | 3 | export class Place { 4 | constructor( 5 | public id: string, 6 | public title: string, 7 | public description: string, 8 | public imageUrl: string, 9 | public price: number, 10 | public availableFrom: Date, 11 | public availableTo: Date, 12 | public userId: string, 13 | public location: PlaceLocation 14 | ) {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/places/places-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule, Routes } from '@angular/router'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { PlacesPage } from './places.page'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'tabs', 9 | component: PlacesPage, 10 | children: [ 11 | { 12 | path: 'discover', 13 | children: [ 14 | { 15 | path: '', 16 | loadChildren: () => import('./discover/discover.module').then(m => m.DiscoverPageModule) 17 | }, 18 | { 19 | path: ':placeId', 20 | loadChildren: () => import('./discover/place-detail/place-detail.module').then(m => m.PlaceDetailPageModule) 21 | } 22 | ] 23 | }, 24 | { 25 | path: 'offers', 26 | children: [ 27 | { 28 | path: '', 29 | loadChildren: () => import('./offers/offers.module').then(m => m.OffersPageModule) 30 | }, 31 | { 32 | path: 'new', 33 | loadChildren: () => import('./offers/new-offer/new-offer.module').then(m => m.NewOfferPageModule) 34 | }, 35 | { 36 | path: 'edit/:placeId', 37 | loadChildren: () => import('./offers/edit-offer/edit-offer.module').then(m => m.EditOfferPageModule) 38 | } 39 | ] 40 | }, 41 | { 42 | path: '', 43 | redirectTo: '/places/tabs/discover', 44 | pathMatch: 'full' 45 | } 46 | ] 47 | }, 48 | { 49 | path: '', 50 | redirectTo: '/places/tabs/discover', 51 | pathMatch: 'full' 52 | } 53 | ]; 54 | 55 | @NgModule({ 56 | imports: [RouterModule.forChild(routes)], 57 | exports: [RouterModule] 58 | }) 59 | export class PlacesRoutingModule {} 60 | -------------------------------------------------------------------------------- /src/app/places/places.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | 5 | import { PlacesPage } from './places.page'; 6 | import { PlacesRoutingModule } from './places-routing.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | IonicModule, 12 | PlacesRoutingModule 13 | ], 14 | declarations: [PlacesPage] 15 | }) 16 | export class PlacesPageModule {} 17 | -------------------------------------------------------------------------------- /src/app/places/places.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Discover 5 | 6 | 7 | 8 | Offers 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/places/places.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/app/places/places.page.scss -------------------------------------------------------------------------------- /src/app/places/places.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { PlacesPage } from './places.page'; 5 | 6 | describe('PlacesPage', () => { 7 | let component: PlacesPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ PlacesPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(PlacesPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/places/places.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-places', 5 | templateUrl: './places.page.html', 6 | styleUrls: ['./places.page.scss'], 7 | }) 8 | export class PlacesPage implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/places/places.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { PlacesService } from './places.service'; 4 | 5 | describe('PlacesService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: PlacesService = TestBed.get(PlacesService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/places/places.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { BehaviorSubject, of } from "rxjs"; 3 | import { take, map, tap, switchMap } from "rxjs/operators"; 4 | import { HttpClient } from "@angular/common/http"; 5 | 6 | import { Place } from "./place.model"; 7 | import { AuthService } from "../auth/auth.service"; 8 | import { PlaceLocation } from "./location.model"; 9 | 10 | interface PlaceData { 11 | availableFrom: string; 12 | availableTo: string; 13 | description: string; 14 | imageUrl: string; 15 | price: number; 16 | title: string; 17 | userId: string; 18 | location: PlaceLocation; 19 | } 20 | 21 | // Enter your project url, e.g. https://ionic-maps-api-1565705298126.firebaseio.com 22 | const projectApiUrl = ""; 23 | // Enter your project image url, e.g. https://us-central1-ionic-maps-api-1565705298126.cloudfunctions.net 24 | const projectImageUrl = "" 25 | 26 | // new Place( 27 | // 'p1', 28 | // 'Manhatten Mansions', 29 | // 'In the heart of New York City', 30 | // 'https://www.ecuavisa.com/cdn-cgi/image/width=1600,quality=75/sites/default/files/fotos/2016/03/15/casa_tyson.jpg', 31 | // 149.99, 32 | // new Date('2019-01-01'), 33 | // new Date('2019-12-31'), 34 | // 'abc' 35 | // ), 36 | // new Place( 37 | // 'p2', 38 | // 'L\'Amour Toujours', 39 | // 'A romantic place in Paris', 40 | // // tslint:disable-next-line: max-line-length 41 | // 'http://www.parisianflat.com/images/apartments/7/living_casimir.jpg', 42 | // 99.99, 43 | // new Date('2019-01-01'), 44 | // new Date('2019-12-31'), 45 | // 'abc' 46 | // ), 47 | // new Place( 48 | // 'p3', 49 | // 'The Foggy Palace', 50 | // 'Not your average day trip', 51 | // // tslint:disable-next-line: max-line-length 52 | // 'https://previews.123rf.com/images/erin71/erin711802/erin71180200003/95887113-hamilton-house-on-foggy-winter-day.jpg', 53 | // 99.99, 54 | // new Date('2019-01-01'), 55 | // new Date('2019-12-31'), 56 | // 'abc' 57 | // ) 58 | 59 | @Injectable({ 60 | providedIn: "root", 61 | }) 62 | export class PlacesService { 63 | private _places = new BehaviorSubject([]); 64 | 65 | get places() { 66 | return this._places.asObservable(); 67 | } 68 | 69 | constructor(private authService: AuthService, private http: HttpClient) {} 70 | 71 | fetchPlaces() { 72 | return this.authService.token.pipe( 73 | take(1), 74 | switchMap((token) => { 75 | return this.http.get<{ [key: string]: PlaceData }>( 76 | `${projectApiUrl}/offered-places.json?auth=${token}` 77 | ); 78 | }), 79 | map((resData) => { 80 | const places = []; 81 | for (const key in resData) { 82 | if (resData.hasOwnProperty(key)) { 83 | places.push( 84 | new Place( 85 | key, 86 | resData[key].title, 87 | resData[key].description, 88 | resData[key].imageUrl, 89 | resData[key].price, 90 | new Date(resData[key].availableFrom), 91 | new Date(resData[key].availableTo), 92 | resData[key].userId, 93 | resData[key].location 94 | ) 95 | ); 96 | } 97 | } 98 | return places; 99 | }), 100 | tap((places) => { 101 | this._places.next(places); 102 | }) 103 | ); 104 | } 105 | 106 | getPlace(id: string) { 107 | return this.authService.token.pipe( 108 | take(1), // only need one token 109 | switchMap((token) => { 110 | return this.http.get( 111 | `${projectApiUrl}/offered-places/${id}.json?auth=${token}` 112 | ); 113 | }), 114 | map((placeData) => { 115 | return new Place( 116 | id, 117 | placeData.title, 118 | placeData.description, 119 | placeData.imageUrl, 120 | placeData.price, 121 | new Date(placeData.availableFrom), 122 | new Date(placeData.availableTo), 123 | placeData.userId, 124 | placeData.location 125 | ); 126 | }) 127 | ); 128 | } 129 | 130 | uploadImage(image: File) { 131 | const uploadData = new FormData(); 132 | uploadData.append("image", image); 133 | 134 | return this.authService.token.pipe( 135 | take(1), 136 | switchMap((token) => { 137 | return this.http.post<{ imageUrl: string; imagePath: string }>( 138 | `${projectImageUrl}/storeImage`, 139 | uploadData, 140 | { headers: { Authorization: "Bearer " + token } } 141 | ); 142 | }) 143 | ); 144 | } 145 | 146 | addPlace( 147 | title: string, 148 | description: string, 149 | price: number, 150 | dateFrom: Date, 151 | dateTo: Date, 152 | location: PlaceLocation, 153 | imageUrl: string 154 | ) { 155 | let generatedId: string; 156 | let fetchedUserId: string; 157 | let newPlace: Place; 158 | return this.authService.userId.pipe( 159 | take(1), 160 | switchMap((userId) => { 161 | fetchedUserId = userId; 162 | return this.authService.token; 163 | }), 164 | take(1), 165 | switchMap((token) => { 166 | if (!fetchedUserId) { 167 | throw new Error("No user found"); 168 | } 169 | newPlace = new Place( 170 | Math.random().toString(), 171 | title, 172 | description, 173 | imageUrl, 174 | price, 175 | dateFrom, 176 | dateTo, 177 | fetchedUserId, 178 | location 179 | ); 180 | // post newPlace data to Firebase but replace id with null 181 | return this.http.post<{ name: string }>( 182 | `${projectApiUrl}/offered-places.json?auth=${token}`, 183 | { 184 | ...newPlace, 185 | id: null, 186 | } 187 | ); 188 | }), 189 | switchMap((resData) => { 190 | generatedId = resData.name; 191 | return this.places; 192 | }), 193 | take(1), 194 | tap((places) => { 195 | newPlace.id = generatedId; 196 | this._places.next(places.concat(newPlace)); 197 | }) 198 | ); 199 | // look through array object and just take 1 item then cancel subscription 200 | // concat takes old array, adds new item then returns new array to be emitted using next() 201 | // return this._places.pipe( 202 | // take(1), 203 | // delay(1000), 204 | // tap(places => { 205 | // this._places.next(places.concat(newPlace)); 206 | // }) 207 | // ); 208 | } 209 | 210 | // emits a new list of offers that includes the added place 211 | // logic ensures there is always a list of offers to work with. 212 | updatePlace(placeId: string, title: string, description: string) { 213 | let updatedPlaces: Place[]; // initialise updated places variable 214 | let fetchedToken: string; 215 | return this.authService.token.pipe( 216 | take(1), 217 | switchMap((token) => { 218 | fetchedToken = token; 219 | return this.places; 220 | }), 221 | take(1), // take latest snapshot of list 222 | switchMap((places) => { 223 | if (!places || places.length <= 0) { 224 | return this.fetchPlaces(); // returns updated array of places 225 | } else { 226 | return of(places); // of takes array and wraps it into a new observable that will emit a value right away 227 | } 228 | }), 229 | switchMap((places) => { 230 | const updatedPlaceIndex = places.findIndex((pl) => pl.id === placeId); 231 | updatedPlaces = [...places]; 232 | const oldPlace = updatedPlaces[updatedPlaceIndex]; 233 | updatedPlaces[updatedPlaceIndex] = new Place( 234 | oldPlace.id, 235 | title, 236 | description, 237 | oldPlace.imageUrl, 238 | oldPlace.price, 239 | oldPlace.availableFrom, 240 | oldPlace.availableTo, 241 | oldPlace.userId, 242 | oldPlace.location 243 | ); 244 | return this.http.put( 245 | `${projectApiUrl}/offered-places/${placeId}.json?auth=${fetchedToken}`, 246 | { ...updatedPlaces[updatedPlaceIndex], id: null } // Data to be replaced, override id 247 | ); 248 | }), 249 | tap(() => { 250 | this._places.next(updatedPlaces); // emit new list of updated places 251 | }) 252 | ); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/app/shared/map-modal/map-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ title }} 4 | 5 | {{ closeButtonText }} 6 | 7 | 8 | 9 | 10 | 11 |
12 |
-------------------------------------------------------------------------------- /src/app/shared/map-modal/map-modal.component.scss: -------------------------------------------------------------------------------- 1 | .map { 2 | position: absolute; 3 | height: 100%; 4 | width: 100%; 5 | 6 | background-color: transparent; 7 | 8 | opacity: 0; 9 | transition: opacity 150mS ease-in; 10 | } 11 | 12 | .map.visible { 13 | opacity: 1; 14 | } -------------------------------------------------------------------------------- /src/app/shared/map-modal/map-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { MapModalComponent } from './map-modal.component'; 5 | 6 | describe('MapModalComponent', () => { 7 | let component: MapModalComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ MapModalComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(MapModalComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shared/map-modal/map-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { ModalController } from '@ionic/angular'; 2 | import { Component, OnInit, AfterViewInit, ElementRef, ViewChild, Renderer2, OnDestroy, Input } from '@angular/core'; 3 | import { environment } from '../../../environments/environment'; 4 | 5 | @Component({ 6 | selector: 'app-map-modal', 7 | templateUrl: './map-modal.component.html', 8 | styleUrls: ['./map-modal.component.scss'], 9 | }) 10 | export class MapModalComponent implements OnInit, AfterViewInit, OnDestroy { 11 | 12 | // ViewChild is a property decorator that configures a view query. 13 | @ViewChild('map') mapElementRef: ElementRef; 14 | @Input() center = { lat: 37.7964333, lng: -1.5121459 }; 15 | @Input() selectable = true; 16 | @Input() closeButtonText = 'Cancel'; 17 | @Input() title = 'Pick Location'; 18 | clickListener: any; 19 | googleMaps: any; 20 | 21 | constructor(private modalCtrl: ModalController, private renderer: Renderer2) { } 22 | 23 | ngOnInit() { 24 | } 25 | 26 | // lifecycle hook that is called after Angular has fully initialized a component's view. 27 | // Used for any additional initialization tasks 28 | ngAfterViewInit() { 29 | this.getGoogleMaps() 30 | .then(googleMaps => { 31 | this.googleMaps = googleMaps; 32 | const mapEl = this.mapElementRef.nativeElement; 33 | const map = new googleMaps.Map(mapEl, { 34 | center: this.center, 35 | zoom: 16 36 | }); 37 | 38 | this.googleMaps.event.addListenerOnce(map, 'idle', () => { 39 | this.renderer.addClass(mapEl, 'visible'); 40 | }); 41 | 42 | if (this.selectable) { 43 | this.clickListener = map.addListener('click', event => { 44 | const selectedCoords = { 45 | lat: event.latLng.lat(), 46 | lng: event.latLng.lng() 47 | }; 48 | this.modalCtrl.dismiss(selectedCoords); 49 | }); 50 | } else { 51 | const marker = new googleMaps.Marker({ 52 | position: this.center, 53 | map, 54 | title: 'Picked Location' 55 | }); 56 | marker.setMap(map); 57 | } 58 | }) 59 | .catch(err => { 60 | console.log(err); 61 | }); 62 | } 63 | 64 | onCancel() { 65 | this.modalCtrl.dismiss(); 66 | } 67 | 68 | ngOnDestroy() { 69 | if (this.clickListener) { 70 | this.googleMaps.event.removeListener(this.clickListener); 71 | } 72 | } 73 | 74 | private getGoogleMaps(): Promise { 75 | const win = window as any; 76 | const googleModule = win.google; 77 | 78 | // check if google maps loaded already, if so go to google maps module 79 | if (googleModule && googleModule.maps) { 80 | return Promise.resolve(googleModule.maps); 81 | } 82 | 83 | // show google maps window as a DOM child script 84 | return new Promise((resolve, reject) => { 85 | const script = document.createElement('script'); 86 | script.src = 87 | 'https://maps.googleapis.com/maps/api/js?key=' + 88 | environment.googleMapsAPIKey; 89 | script.async = true; 90 | script.defer = true; 91 | document.body.appendChild(script); 92 | 93 | // script listener is an anonymous arrow function 94 | script.onload = () => { 95 | const loadedGoogleModule = win.google; 96 | if (loadedGoogleModule && loadedGoogleModule.maps) { 97 | resolve(loadedGoogleModule.maps); 98 | } else { 99 | reject ('Google Maps SDK not available'); 100 | } 101 | }; 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/shared/pickers/image-picker/image-picker.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 9 | 10 | 14 | 15 | Take Picture 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/app/shared/pickers/image-picker/image-picker.component.scss: -------------------------------------------------------------------------------- 1 | .picker { 2 | width: 30rem; 3 | max-width: 80%; 4 | height: 20rem; 5 | max-height: 30vh; 6 | border: 1px solid var(--ion-color-primary); 7 | margin: auto; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .image { 14 | width: 100%; 15 | height: 100%; 16 | object-fit: cover; 17 | } 18 | 19 | input[type='file'] { 20 | display: false; 21 | } -------------------------------------------------------------------------------- /src/app/shared/pickers/image-picker/image-picker.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { ImagePickerComponent } from './image-picker.component'; 5 | 6 | describe('ImagePickerComponent', () => { 7 | let component: ImagePickerComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ ImagePickerComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(ImagePickerComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shared/pickers/image-picker/image-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Output, EventEmitter, ElementRef, ViewChild, Input } from '@angular/core'; 2 | import { Plugins, Capacitor, CameraSource, CameraResultType } from '@capacitor/core'; 3 | import { Platform } from '@ionic/angular'; 4 | 5 | @Component({ 6 | selector: 'app-image-picker', 7 | templateUrl: './image-picker.component.html', 8 | styleUrls: ['./image-picker.component.scss'], 9 | }) 10 | export class ImagePickerComponent implements OnInit { 11 | @ViewChild('filePicker') filePickerRef: ElementRef; 12 | @Output() imagePick = new EventEmitter(); 13 | @Input() showPreview = false; 14 | selectedImage: string; 15 | usePicker = false; 16 | 17 | constructor(private platform: Platform) { } 18 | 19 | ngOnInit() { 20 | // console.log('Mobile', this.platform.is('mobile')); 21 | // console.log('Hybrid', this.platform.is('hybrid')); 22 | // console.log('iOS', this.platform.is('ios')); 23 | // console.log('Android', this.platform.is('android')); 24 | // console.log('Desktop', this.platform.is('desktop')); 25 | 26 | if ((this.platform.is('mobile') && !this.platform.is('hybrid')) || 27 | this.platform.is('desktop') 28 | ) { 29 | this.usePicker = true; 30 | } 31 | } 32 | 33 | onPickImage() { 34 | if (!Capacitor.isPluginAvailable('Camera')) { 35 | this.filePickerRef.nativeElement.click(); 36 | return; 37 | } 38 | Plugins.Camera 39 | .getPhoto({ 40 | quality: 50, 41 | source: CameraSource.Prompt, 42 | correctOrientation: true, 43 | width: 200, 44 | // choose a base64 string so it can be easily converted to an image file 45 | resultType: CameraResultType.Base64 46 | }) 47 | .then(image => { 48 | // added code 'data:image/jpeg;base64' to make image show in browser 49 | this.selectedImage = 'data:image/jpeg;base64,' + image.base64String; 50 | // emit photo as a base 64 string 51 | this.imagePick.emit(image.base64String); 52 | }) 53 | .catch(error => { 54 | console.log(error); 55 | if (this.usePicker) { 56 | this.filePickerRef.nativeElement.click(); 57 | } 58 | return false; 59 | }); 60 | } 61 | 62 | onFileChosen(event: Event) { 63 | const pickedFile = (event.target as HTMLInputElement).files[0]; 64 | if (!pickedFile) { 65 | return; 66 | } 67 | const fileReader = new FileReader(); 68 | fileReader.onload = () => { 69 | const dataUrl = fileReader.result.toString(); 70 | this.selectedImage = dataUrl; 71 | this.imagePick.emit(pickedFile); 72 | }; 73 | fileReader.readAsDataURL(pickedFile); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/shared/pickers/location-picker/location-picker.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 10 | 11 | 15 | 16 | Select Location 17 | 18 |
-------------------------------------------------------------------------------- /src/app/shared/pickers/location-picker/location-picker.component.scss: -------------------------------------------------------------------------------- 1 | .picker { 2 | width: 30rem; 3 | max-width: 80%; 4 | height: 20rem; 5 | max-height: 30vh; 6 | border: 1px solid var(--ion-color-primary); 7 | margin: auto; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .location-image { 14 | width: 100%; 15 | height: 100%; 16 | object-fit: cover; 17 | } -------------------------------------------------------------------------------- /src/app/shared/pickers/location-picker/location-picker.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { LocationPickerComponent } from './location-picker.component'; 5 | 6 | describe('LocationPickerComponent', () => { 7 | let component: LocationPickerComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ LocationPickerComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(LocationPickerComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shared/pickers/location-picker/location-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, EventEmitter, Output, Input } from '@angular/core'; 2 | import { ModalController, ActionSheetController, AlertController } from '@ionic/angular'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { switchMap, map } from 'rxjs/operators'; 5 | import { of } from 'rxjs'; 6 | import { Plugins, Capacitor } from '@capacitor/core'; 7 | 8 | import { MapModalComponent } from '../../map-modal/map-modal.component'; 9 | import { environment } from '../../../../environments/environment'; 10 | import { PlaceLocation, Coordinates } from '../../../places/location.model'; 11 | 12 | @Component({ 13 | selector: 'app-location-picker', 14 | templateUrl: './location-picker.component.html', 15 | styleUrls: ['./location-picker.component.scss'], 16 | }) 17 | export class LocationPickerComponent implements OnInit { 18 | @Output() locationPick = new EventEmitter(); 19 | @Input() showPreview = false; 20 | selectedLocationImage: string; 21 | isLoading = false; 22 | 23 | constructor( 24 | private modalCtrl: ModalController, 25 | private http: HttpClient, 26 | private actionSheetCtrl: ActionSheetController, 27 | private alertCtrl: AlertController 28 | ) { } 29 | 30 | ngOnInit() {} 31 | 32 | onPickLocation() { 33 | this.actionSheetCtrl 34 | .create({ 35 | header: 'Please Choose', 36 | buttons: [ 37 | {text: 'Auto-locate', handler: () => { 38 | this.locateUser(); 39 | }}, 40 | {text: 'Pick on Map', handler: () => { 41 | this.openMap(); 42 | }}, 43 | {text: 'Cancel', role: 'cancel'} 44 | ] 45 | }) 46 | .then(actionSheetEl => { 47 | actionSheetEl.present(); 48 | }); 49 | 50 | } 51 | 52 | private locateUser() { 53 | if (!Capacitor.isPluginAvailable('Geolocation')) { 54 | this.showErrorAlert(); 55 | return; 56 | } 57 | this.isLoading = true; 58 | Plugins.Geolocation.getCurrentPosition() 59 | .then(geoPosition => { 60 | const coordinates: Coordinates = { 61 | lat: geoPosition.coords.latitude, 62 | lng: geoPosition.coords.longitude 63 | }; 64 | this.createPlace(coordinates.lat, coordinates.lng); 65 | this.isLoading = false; 66 | }) 67 | .catch(err => { 68 | this.showErrorAlert(); 69 | this.isLoading = false; 70 | }); 71 | } 72 | 73 | private showErrorAlert() { 74 | this.alertCtrl 75 | .create({ 76 | header: 'Could not fetch location', 77 | message: 'Please use the map to pick a location', 78 | buttons: ['OK'] 79 | }) 80 | .then(alertEl => alertEl.present()); 81 | } 82 | 83 | private openMap() { 84 | this.modalCtrl.create({ component: MapModalComponent }).then(modalEl => { 85 | modalEl.onDidDismiss().then(modalData => { 86 | if (!modalData.data) { 87 | return; 88 | } 89 | const coordinates: Coordinates = { 90 | lat: modalData.data.lat, 91 | lng: modalData.data.lng 92 | }; 93 | this.createPlace(coordinates.lat, coordinates.lng); 94 | }); 95 | modalEl.present(); 96 | }); 97 | } 98 | 99 | private createPlace(lat: number, lng: number) { 100 | const pickedLocation: PlaceLocation = { 101 | lat, 102 | lng, 103 | address: null, 104 | staticMapImageUrl: null 105 | }; 106 | this.isLoading = true; 107 | this.getAddress(lat, lng) 108 | .pipe( 109 | switchMap(address => { 110 | pickedLocation.address = address; 111 | return of( 112 | this.getMapImage(pickedLocation.lat, pickedLocation.lng, 14) 113 | ); 114 | }) 115 | ) 116 | .subscribe(staticMapImageUrl => { 117 | pickedLocation.staticMapImageUrl = staticMapImageUrl; 118 | this.selectedLocationImage = staticMapImageUrl; 119 | this.isLoading = false; 120 | this.locationPick.emit(pickedLocation); 121 | }); 122 | } 123 | 124 | // helper function to get address from google maps api using lat and lng input coords 125 | private getAddress(lat: number, lng: number) { 126 | return this.http 127 | .get( 128 | `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${ 129 | environment.googleMapsAPIKey 130 | }` 131 | ) 132 | .pipe( 133 | map(geoData => { 134 | if (!geoData || !geoData.results || geoData.results.length === 0) { 135 | return null; 136 | } 137 | return geoData.results[0].formatted_address; 138 | }) 139 | ); 140 | } 141 | 142 | private getMapImage(lat: number, lng: number, zoom: number) { 143 | return `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=${zoom}&size=500x300&maptype=roadmap 144 | &markers=color:red%7Clabel:Place%7C${lat},${lng} 145 | &key=${environment.googleMapsAPIKey}`; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { IonicModule } from '@ionic/angular'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | import { LocationPickerComponent } from './pickers/location-picker/location-picker.component'; 6 | import { MapModalComponent } from './map-modal/map-modal.component'; 7 | import { ImagePickerComponent } from './pickers/image-picker/image-picker.component'; 8 | 9 | 10 | @NgModule({ 11 | declarations: [LocationPickerComponent, MapModalComponent, ImagePickerComponent], 12 | imports: [CommonModule, IonicModule], 13 | exports: [LocationPickerComponent, MapModalComponent, ImagePickerComponent] 14 | }) 15 | export class SharedModule {} 16 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-project/054e1af7bd70cda00219fcc7eb21521afa5a7f01/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | googleMapsAPIKey: '', 4 | firebaseAPIKey: '' 5 | }; 6 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | googleMapsAPIKey: '', 8 | firebaseAPIKey: '' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /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 | @import '~@ionic/angular/css/display.css'; 20 | 21 | /* Optional CSS utils that can be commented out */ 22 | @import "~@ionic/angular/css/padding.css"; 23 | @import "~@ionic/angular/css/float-elements.css"; 24 | @import "~@ionic/angular/css/text-alignment.css"; 25 | @import "~@ionic/angular/css/text-transformation.css"; 26 | @import "~@ionic/angular/css/flex-utils.css"; 27 | 28 | .ion-invalid.ion-touched ion-label { 29 | color: var(--ion-color-danger); 30 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { defineCustomElements } from '@ionic/pwa-elements/loader'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | 15 | defineCustomElements(window); 16 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "short_name": "app", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "assets/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "assets/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "assets/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "assets/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "assets/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "assets/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "assets/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /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 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags.ts'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | import './zone-flags.ts'; 46 | 47 | /*************************************************************************************************** 48 | * Zone JS is required by default for Angular itself. 49 | */ 50 | 51 | import 'zone.js'; // Included with Angular CLI. 52 | 53 | 54 | /*************************************************************************************************** 55 | * APPLICATION IMPORTS 56 | */ 57 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), { 16 | teardown: { destroyAfterEach: false } 17 | } 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | --ion-color-primary: #f72664; 7 | --ion-color-primary-rgb: 247,38,100; 8 | --ion-color-primary-contrast: #ffffff; 9 | --ion-color-primary-contrast-rgb: 255,255,255; 10 | --ion-color-primary-shade: #d92158; 11 | --ion-color-primary-tint: #f83c74; 12 | 13 | --ion-color-secondary: #ac0e86; 14 | --ion-color-secondary-rgb: 172,14,134; 15 | --ion-color-secondary-contrast: #ffffff; 16 | --ion-color-secondary-contrast-rgb: 255,255,255; 17 | --ion-color-secondary-shade: #970c76; 18 | --ion-color-secondary-tint: #b42692; 19 | 20 | --ion-color-tertiary: #8400ff; 21 | --ion-color-tertiary-rgb: 132,0,255; 22 | --ion-color-tertiary-contrast: #ffffff; 23 | --ion-color-tertiary-contrast-rgb: 255,255,255; 24 | --ion-color-tertiary-shade: #7400e0; 25 | --ion-color-tertiary-tint: #901aff; 26 | 27 | --ion-color-success: #75cc53; 28 | --ion-color-success-rgb: 117,204,83; 29 | --ion-color-success-contrast: #000000; 30 | --ion-color-success-contrast-rgb: 0,0,0; 31 | --ion-color-success-shade: #67b449; 32 | --ion-color-success-tint: #83d164; 33 | 34 | --ion-color-warning: #ffb732; 35 | --ion-color-warning-rgb: 255,183,50; 36 | --ion-color-warning-contrast: #000000; 37 | --ion-color-warning-contrast-rgb: 0,0,0; 38 | --ion-color-warning-shade: #e0a12c; 39 | --ion-color-warning-tint: #ffbe47; 40 | 41 | --ion-color-danger: #f04023; 42 | --ion-color-danger-rgb: 240,64,35; 43 | --ion-color-danger-contrast: #ffffff; 44 | --ion-color-danger-contrast-rgb: 255,255,255; 45 | --ion-color-danger-shade: #d3381f; 46 | --ion-color-danger-tint: #f25339; 47 | 48 | --ion-color-dark: #222428; 49 | --ion-color-dark-rgb: 34,34,34; 50 | --ion-color-dark-contrast: #ffffff; 51 | --ion-color-dark-contrast-rgb: 255,255,255; 52 | --ion-color-dark-shade: #1e2023; 53 | --ion-color-dark-tint: #383a3e; 54 | 55 | --ion-color-medium: #989aa2; 56 | --ion-color-medium-rgb: 152,154,162; 57 | --ion-color-medium-contrast: #ffffff; 58 | --ion-color-medium-contrast-rgb: 255,255,255; 59 | --ion-color-medium-shade: #86888f; 60 | --ion-color-medium-tint: #a2a4ab; 61 | 62 | --ion-color-light: #f4f5f8; 63 | --ion-color-light-rgb: 244,244,244; 64 | --ion-color-light-contrast: #000000; 65 | --ion-color-light-contrast-rgb: 0,0,0; 66 | --ion-color-light-shade: #d7d8da; 67 | --ion-color-light-tint: #f5f6f9; 68 | 69 | --ion-toolbar-background: var(--ion-color-primary); 70 | --ion-toolbar-color: var(--ion-color-primary-contrast); 71 | 72 | } 73 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "src/test.ts", 12 | "src/**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "skipLibCheck": true, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ], 22 | "skipLibCheck": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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/zone-flags.ts", 13 | "src/polyfills.ts" 14 | ], 15 | "include": [ 16 | "src/**/*.spec.ts", 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /typings/cordova-typings.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// --------------------------------------------------------------------------------