├── .eslintrc.json ├── .firebase └── hosting.d3d3.cache ├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── browserslist ├── config.xml ├── e2e ├── protractor.conf.js ├── src │ └── app.po.ts └── tsconfig.json ├── firebase.json ├── img ├── about1.png ├── about2.png ├── about3.png ├── android-aboutdark.png ├── android-aboutlight.png ├── android-categoriesdark.png ├── android-detaildark.png ├── android-detaildark1.png ├── android-detaillight.png ├── android-favouriteslight.png ├── android-menudark.png ├── android-menulight.png ├── categories1.png ├── categories2.png ├── categories3.png ├── dark1.png ├── dark2.png ├── dark3.png ├── favourites1.png ├── favourites2.png ├── favourites3.png ├── news-detail1.png ├── news-detail2.png ├── news-detail3.png ├── news1.png ├── news2.png └── news3.png ├── ionic.config.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── resources ├── README.md ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ ├── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png │ └── xml │ │ └── network_security_config.xml ├── icon.png ├── ios │ ├── icon │ │ ├── icon-1024.png │ │ ├── icon-20.png │ │ ├── icon-20@2x.png │ │ ├── icon-20@3x.png │ │ ├── icon-24@2x.png │ │ ├── icon-27.5@2x.png │ │ ├── icon-29.png │ │ ├── icon-29@2x.png │ │ ├── icon-29@3x.png │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-44@2x.png │ │ ├── icon-50.png │ │ ├── icon-50@2x.png │ │ ├── icon-60.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72.png │ │ ├── icon-72@2x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ ├── icon-83.5@2x.png │ │ ├── icon-86@2x.png │ │ ├── icon-98@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ ├── icon-small@3x.png │ │ ├── icon.png │ │ └── icon@2x.png │ └── splash │ │ ├── Default-2436h.png │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default-Landscape-2436h.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Landscape@~ipadpro.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait@~ipadpro.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default@2x~iphone.png │ │ ├── Default@2x~universal~anyany.png │ │ └── Default~iphone.png └── splash.png ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── app.scss │ ├── components │ │ ├── article-list │ │ │ ├── article-list.component.html │ │ │ ├── article-list.component.scss │ │ │ └── article-list.component.ts │ │ ├── page-refresh │ │ │ ├── page-refresh.component.html │ │ │ ├── page-refresh.component.scss │ │ │ └── page-refresh.component.ts │ │ ├── progress-bar │ │ │ ├── progress-bar.component.html │ │ │ ├── progress-bar.component.scss │ │ │ └── progress-bar.component.ts │ │ └── svgs │ │ │ ├── news-svg │ │ │ └── news-svg.component.ts │ │ │ └── svg.component.scss │ ├── interfaces │ │ └── interfaces.ts │ ├── pages │ │ ├── about-popover │ │ │ ├── about-popover.html │ │ │ ├── about-popover.scss │ │ │ └── about-popover.ts │ │ ├── about │ │ │ ├── about.page.html │ │ │ ├── about.page.scss │ │ │ └── about.page.ts │ │ ├── categories │ │ │ ├── categories.page.html │ │ │ ├── categories.page.scss │ │ │ └── categories.page.ts │ │ ├── favourites │ │ │ ├── favourites-popover │ │ │ │ └── favourites-popover.ts │ │ │ ├── favourites.page.html │ │ │ ├── favourites.page.scss │ │ │ └── favourites.page.ts │ │ ├── news-detail │ │ │ ├── news-detail.page.html │ │ │ ├── news-detail.page.scss │ │ │ └── news-detail.page.ts │ │ ├── news │ │ │ ├── news.page.html │ │ │ ├── news.page.scss │ │ │ └── news.page.ts │ │ └── tabs │ │ │ ├── tabs.module.ts │ │ │ ├── tabs.page.html │ │ │ ├── tabs.page.scss │ │ │ ├── tabs.page.ts │ │ │ └── tabs.router.module.ts │ ├── pipes │ │ ├── date-convert.pipe.ts │ │ ├── pipes.module.ts │ │ ├── title-convert.pipe.ts │ │ └── title-nosource.pipe.ts │ └── providers │ │ ├── language.service.ts │ │ ├── network.service.ts │ │ ├── news-api.service.ts │ │ ├── storage.service.ts │ │ ├── theme.service.ts │ │ └── toast.service.ts ├── assets │ ├── i18n │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ └── sp.json │ ├── icon │ │ └── favicon.ico │ ├── imgs │ │ ├── about.jpg │ │ ├── en.png │ │ ├── fr.png │ │ ├── not-found.jpg │ │ └── sp.png │ ├── pages-data.ts │ └── svgs │ │ └── newspaper.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── main.server.ts ├── main.ts ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss └── zone-flags.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── typings └── cordova-typings.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "extends": "eslint:recommended", 6 | "parser": "@babel/eslint-parser", 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "legacyDecorators": true, 10 | "jsx": true, 11 | "modules": true 12 | }, 13 | "ecmaVersion": 6, 14 | "sourceType": "module", 15 | "allowImportExportEverywhere": true 16 | }, 17 | "rules": { 18 | "strict": 0, 19 | "indent": 0, 20 | "linebreak-style": 0, 21 | "quotes": [ 22 | "error", 23 | "double" 24 | ], 25 | "semi": [ 26 | "error", 27 | "always" 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ionic-angular-news" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.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 | .angular/ 17 | .firebase/ 18 | .idea/ 19 | .ionic/ 20 | .sourcemaps/ 21 | .sass-cache/ 22 | .tmp/ 23 | .versions/ 24 | coverage/ 25 | www/ 26 | dist/ 27 | node_modules/ 28 | tmp/ 29 | temp/ 30 | platforms/ 31 | plugins/ 32 | plugins/android.json 33 | plugins/ios.json 34 | $RECYCLE.BIN/ 35 | 36 | .DS_Store 37 | Thumbs.db 38 | UserInterfaceState.xcuserstate 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AndrewJBateman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :zap: Ionic Angular News App 2 | 3 | * Displays news items from a [news API](https://newsapi.org/) using the [Ionic framework](https://ionicframework.com/docs). 4 | * **Note:** to open web links in a new window use: _ctrl+click on link_ 5 | 6 | ![GitHub repo size](https://img.shields.io/github/repo-size/AndrewJBateman/ionic-angular-news-app?style=plastic) 7 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/AndrewJBateman/ionic-angular-news-app?style=plastic) 8 | ![GitHub Repo stars](https://img.shields.io/github/stars/AndrewJBateman/ionic-angular-news-app?style=plastic) 9 | ![GitHub last commit](https://img.shields.io/github/last-commit/AndrewJBateman/ionic-angular-news-app?style=plastic) 10 | 11 | ## :page_facing_up: Table of contents 12 | 13 | * [:zap: Ionic Angular News App](#zap-ionic-angular-news-app) 14 | * [:page\_facing\_up: Table of contents](#page_facing_up-table-of-contents) 15 | * [:books: General info](#books-general-info) 16 | * [:camera: Screenshots](#camera-screenshots) 17 | * [:signal\_strength: Technologies](#signal_strength-technologies) 18 | * [:floppy\_disk: Setup](#floppy_disk-setup) 19 | * [:computer: Code Examples](#computer-code-examples) 20 | * [:cool: Features](#cool-features) 21 | * [:books: Navigation/Pages](#books-navigationpages) 22 | * [:clipboard: Status](#clipboard-status) 23 | * [:clipboard: To-do](#clipboard-to-do) 24 | * [:clap: Inspiration](#clap-inspiration) 25 | * [:file\_folder: License](#file_folder-license) 26 | * [:envelope: Contact](#envelope-contact) 27 | 28 | ## :books: General info 29 | 30 | * The [News API](https://newsapi.org/) is a simple HTTP REST API for searching and retrieving live articles from the web. 31 | * The [News API](https://newsapi.org/) now only works on localhost. It will not work when deployed due to CORS errors (error 406) which means they want you to pay a subscription to fully access the API. This app was successfully deployed to Android Studio - see screen shots below but I deleted the firebase depoyment. I will do another news app using the [Gnews](https://gnews.io/) API which has a free tier for up to 100 requests per day and 10 articles per search. 32 | * [Codium AI](https://www.codium.ai/) used to check and improve code quality. 33 | 34 | ## :camera: Screenshots 35 | 36 | | ![Ionic page](./img/news1.png) | ![Ionic page](./img/news2.png) | ![Ionic page](./img/news3.png) | 37 | | :----------------------------: | :----------------------------: | :---------------------------------------: | 38 | | News Page French | News Page Spanish | News Page English | 39 | 40 | | ![Ionic page](./img/news-detail1.png) | ![Ionic page](./img/news-detail2.png) | ![Ionic page](./img/news-detail3.png) | 41 | | :----------------------------: | :----------------------------: | :---------------------------------------: | 42 | | News Detail Page French | News Detail Page Spanish | News Detail Page English | 43 | 44 | | ![Ionic page](./img/categories1.png) | ![Ionic page](./img/categories2.png) | ![Ionic page](./img/categories3.png) | 45 | | :----------------------------------: | :----------------------------------: | :---------------------------------------------: | 46 | | Categories Page Business | Categories Page Entertainment | Article Detail Page | 47 | 48 | | ![Ionic page](./img/favourites1.png) | ![Ionic page](./img/favourites2.png) | ![Ionic page](./img/favourites3.png) | 49 | | :----------------------------------: | :----------------------------------: | :---------------------------------------------: | 50 | | Favourites Page Empty | Favourites Page Some | Favourites Page Full | 51 | 52 | | ![Ionic page](./img/about1.png) | ![Ionic page](./img/about2.png) | ![Ionic page](./img/about3.png) | 53 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: | 54 | | About Page French | About Page + Side Menu | About Page + Info Menu | 55 | 56 | | ![Ionic page](./img/dark1.png) | ![Ionic page](./img/dark2.png) | ![Ionic page](./img/dark3.png) | 57 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: | 58 | | Dark Mode News Page | Dark Mode Categories+Menu Page | Dark Mode About Page | 59 | 60 | | ![Ionic page](./img/android-aboutdark.png) | ![Ionic page](./img/android-aboutlight.png) | ![Ionic page](./img/android-categoriesdark.png) | 61 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: | 62 | | Android About Dark Page | Android About Light Page | Android Categories Dark Page | 63 | 64 | | ![Ionic page](./img/android-detaildark.png) | ![Ionic page](./img/android-detaildark1.png) | ![Ionic page](./img/android-detaillight.png) | 65 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: | 66 | | Android Detail Dark Page | Android Detail Dark Page | Android Detail Light Page | 67 | 68 | | ![Ionic page](./img/android-favouriteslight.png) | ![Ionic page](./img/android-menudark.png) | ![Ionic page](./img/android-menulight.png) | 69 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: | 70 | | Android Favourites Light Page | Android Menu Dark Page | Android Menu Light Page | 71 | 72 | ## :signal_strength: Technologies 73 | 74 | * [Ionic v7](https://ionicframework.com/) 75 | * [Angular v17](https://angular.io/) 76 | * [Ionic/angular v7](https://www.npmjs.com/package/@ionic/angular) 77 | * [RxJS v7](https://reactivex.io/) 78 | * [News REST API](https://newsapi.org/) used to search for news articles. Requires API key 79 | * [IP Geolocation API](https://ipapi.co/#api) 80 | * [Ionic Storage v3](https://www.npmjs.com/package/@ionic/storage-angular) specific to Angular 81 | * [Ionic ngx-Translate core v14](https://ionicframework.com/docs/v3/developer-resources/ng2-translate/) 82 | * [Ionic Native Network v5](https://ionicframework.com/docs/native/network) 83 | * [NGX-Translate internationalization library for Angular](http://www.ngx-translate.com/) 84 | * [Ionic open source Ionicons](https://ionicons.com/) 85 | * [Day.js Date Conversion module v1](https://www.npmjs.com/package/dayjs) 86 | 87 | ## :floppy_disk: Setup 88 | 89 | * It is necessary to [register with news API](https://newsapi.org/docs/get-started) to get an API key that is stored in the `environment.ts` file 90 | * To start the server on _localhost://8100_ type: `ionic serve` 91 | * to add android platform: `ionic cordova platform add android` 92 | * to create build file for android: `ionic cordova build android` 93 | * to run on device plugged in via USB cable: `ionic cordova run android` 94 | * Follow this link [to deploy to IOS or Android](https://ionicframework.com/docs/angular/your-first-app/6-deploying-mobile) 95 | 96 | ## :computer: Code Examples 97 | 98 | * service to switch between dark/light display mode 99 | 100 | ```typescript 101 | // enable dark or light mode from HTML toggle switch event via changeThemeMode() function 102 | export class ThemeService implements OnInit{ 103 | darkMode: boolean; 104 | renderer: Renderer2; 105 | 106 | constructor ( 107 | private rendererFactory: RendererFactory2, 108 | private storage: Storage, 109 | @Inject(DOCUMENT) private document: Document 110 | ) { 111 | this.renderer = this.rendererFactory.createRenderer(null, null); 112 | } 113 | 114 | async ngOnInit() { 115 | await this.storage.create(); 116 | } 117 | 118 | enableDark() { 119 | this.renderer.addClass(this.document.body, "dark-theme"); 120 | this.storage.set("dark-theme", true); 121 | this.darkMode = true; 122 | } 123 | 124 | enableLight() { 125 | this.renderer.removeClass(this.document.body, "dark-theme"); 126 | this.storage.set("dark-theme", false); 127 | this.darkMode = false; 128 | } 129 | 130 | changeThemeMode(e: any) { 131 | e.detail.checked ? this.enableDark() : this.enableLight(); 132 | } 133 | } 134 | ``` 135 | 136 | ## :cool: Features 137 | 138 | * **ng Control Flow** latest `@if` and `@for` used in templates 139 | * **Typescript interface** used to define the expected structures of the JSON objects returned from the news API 140 | * **Separate providers (services)** page with API HTTP fetch RxJS observables 141 | * **Custom pipes** used to modify API news article titles, contents and derive '..time ago' from a date string 142 | * **Dark mode** Menu toggle changes from light to dark mode 143 | * **Offline Storage** of dark mode status & favourite articles using Ionic Storage 144 | * **Common Refresh Component** dragging down will perform refresh function 145 | * **Common Progess Bar Component** ion-card shows while news loading on News, Categories and Favourites pages 146 | * **Localisation using i18n** so user can select between English (default), Spanish and French 147 | * **[Ionic colour generator](https://ionicframework.com/docs/theming/color-generator)** used to create color palette 148 | 149 | ## :books: Navigation/Pages 150 | 151 | * **Nav side-bar:** news, categories, favorites, search, about, change language, dark theme toggle + Unsplash image with credit. Sidemenu is dismissed when the user clicks on a list item. 152 | * **News page** shows world headlines using an ion-card list. Uses `@if` control flow to only show card if it has an image to avoid having news items with empty spaces (API data is not perfect). Shows time as '... ago' using a date convert pipe that uses day.js to convert the API Coordinated Universal Time (UTC) date-time string to '...ago'. 153 | * **News-detail page** shows the selected news item in more detail. Title has news source end text removed using a custom Angular pipe as I show this information in the top toolbar. Also uses custom pipe to show time as '... ago'. Includes working footer buttons for 'More info', which opens news source in a separate window and 'Favourite' which adds the article to a stored news 'favourites' array. Array symbol at end of article content string replaced with text using split and concat. **Remove `
  • ` from content text using regex** . 154 | * **Categories page:** ion-segment used to show categories in a scrollable horizontal menu: Sport, Busines, Health, Technology, Science, General, Entertainment. So far categories only shown from English sources. Shows time as '... ago'. 155 | * **Favourites page:** articles listed in reverse date-time order that have been saved by clicking on the favourites icon on the news-detail page. **Include popover that will let user delete all list items, sliding from the right deletes the favourite, prevent storage of duplicate articles. Add 'delete all' button at top. lhs sliding delete is not working.** 156 | * **About page** Displays Unsplash image with author credit and short info about the app with links to APIs used. Header includes popover with links to Author Website, Github Repo and a Contact Page. 157 | 158 | ## :clipboard: Status 159 | 160 | * Status: Working except including language on start-up menu, production build file created, successfully deployed to Android Studio 161 | 162 | ## :clipboard: To-do 163 | 164 | * Disable clicking on menu icon when in news page. 165 | 166 | ## :clap: Inspiration 167 | 168 | * [Angular Standalone Components Unleashed: Exploring the Magic of a World Without NgModule](https://blogs.halodoc.io/angular-standalone-components-unleashed-exploring-the-magic-of-a-world-without-ngmodule/) 169 | * Some of project structure based on: [Ionic example app: 'A conference app built with Ionic to demonstrate Ionic'](https://github.com/ionic-team/ionic-conference-app) 170 | * The code for checking network status is based on: [Ionic 4 Network Check Example Problem](https://forum.ionicframework.com/t/ionic-4-network-check-example-problem/157909/2) 171 | * [Ionic Academy Tutorial: How to Localise Your Ionic App with ngx-translate](https://ionicacademy.com/localise-ionic-ngx-translate/) however language selected using ion-select-option dropdown list in side-menu (ie not using a popover page) 172 | * [Regexr.com](https://regexr.com/) for developing and testing regex expressions 173 | * [Shields badges for readme](https://shields.io) 174 | * [Easy-Resize to resize images to a smaller file size](https://www.easy-resize.com/en/) 175 | * [Font Awesome Free Icon svgs](https://fontawesome.com/icons?d=gallery&m=free) 176 | 177 | ## :file_folder: License 178 | 179 | * This project is licensed under the terms of the MIT license. 180 | 181 | ## :envelope: Contact 182 | 183 | * Repo created by [ABateman](https://github.com/AndrewJBateman), email: `gomezbateman@yahoo.com` 184 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/app/browser", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | { 23 | "glob": "**/*", 24 | "input": "src/assets", 25 | "output": "assets" 26 | }, 27 | { 28 | "glob": "**/*.svg", 29 | "input": "node_modules/ionicons/dist/ionicons/svg", 30 | "output": "./svg" 31 | } 32 | ], 33 | "styles": [ 34 | { 35 | "input": "src/theme/variables.scss" 36 | }, 37 | { 38 | "input": "src/global.scss" 39 | } 40 | ], 41 | "scripts": [], 42 | "aot": false, 43 | "vendorChunk": true, 44 | "extractLicenses": false, 45 | "buildOptimizer": false, 46 | "sourceMap": true, 47 | "optimization": false, 48 | "namedChunks": true 49 | }, 50 | "configurations": { 51 | "production": { 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ], 58 | "optimization": true, 59 | "outputHashing": "all", 60 | "sourceMap": false, 61 | "namedChunks": false, 62 | "aot": true, 63 | "extractLicenses": true, 64 | "vendorChunk": false, 65 | "buildOptimizer": true, 66 | "budgets": [ 67 | { 68 | "type": "initial", 69 | "maximumWarning": "2mb", 70 | "maximumError": "5mb" 71 | }, 72 | { 73 | "type": "anyComponentStyle", 74 | "maximumWarning": "6kb" 75 | } 76 | ] 77 | }, 78 | "ci": { 79 | "budgets": [ 80 | { 81 | "type": "anyComponentStyle", 82 | "maximumWarning": "6kb" 83 | } 84 | ], 85 | "progress": false 86 | } 87 | } 88 | }, 89 | "serve": { 90 | "builder": "@angular-devkit/build-angular:dev-server", 91 | "options": { 92 | "buildTarget": "app:build" 93 | }, 94 | "configurations": { 95 | "production": { 96 | "buildTarget": "app:build:production" 97 | }, 98 | "ci": { 99 | } 100 | } 101 | }, 102 | "extract-i18n": { 103 | "builder": "@angular-devkit/build-angular:extract-i18n", 104 | "options": { 105 | "buildTarget": "app:build" 106 | } 107 | }, 108 | "test": { 109 | "builder": "@angular-devkit/build-angular:karma", 110 | "options": { 111 | "main": "src/test.ts", 112 | "polyfills": "src/polyfills.ts", 113 | "tsConfig": "tsconfig.spec.json", 114 | "karmaConfig": "karma.conf.js", 115 | "styles": [], 116 | "scripts": [], 117 | "assets": [ 118 | { 119 | "glob": "favicon.ico", 120 | "input": "src/", 121 | "output": "/" 122 | }, 123 | { 124 | "glob": "**/*", 125 | "input": "src/assets", 126 | "output": "/assets" 127 | } 128 | ] 129 | }, 130 | "configurations": { 131 | "ci": { 132 | "progress": false, 133 | "watch": false 134 | } 135 | } 136 | }, 137 | "e2e": { 138 | "builder": "@angular-devkit/build-angular:protractor", 139 | "options": { 140 | "protractorConfig": "e2e/protractor.conf.js", 141 | "devServerTarget": "app:serve" 142 | }, 143 | "configurations": { 144 | "production": { 145 | "devServerTarget": "app:serve:production" 146 | }, 147 | "ci": { 148 | "devServerTarget": "app:serve:ci" 149 | } 150 | } 151 | }, 152 | "ionic-cordova-build": { 153 | "builder": "@ionic/angular-toolkit:cordova-build", 154 | "options": { 155 | "browserTarget": "app:build" 156 | }, 157 | "configurations": { 158 | "production": { 159 | "browserTarget": "app:build:production" 160 | } 161 | } 162 | }, 163 | "ionic-cordova-serve": { 164 | "builder": "@ionic/angular-toolkit:cordova-serve", 165 | "options": { 166 | "cordovaBuildTarget": "app:ionic-cordova-build", 167 | "devServerTarget": "app:serve" 168 | }, 169 | "configurations": { 170 | "production": { 171 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 172 | "devServerTarget": "app:serve:production" 173 | } 174 | } 175 | }, 176 | "prerender": { 177 | "builder": "@angular-devkit/build-angular:prerender", 178 | "options": { 179 | "browserTarget": "app:build:production", 180 | "serverTarget": "app:server:production", 181 | "routes": [ 182 | "/" 183 | ] 184 | }, 185 | "configurations": { 186 | "production": {} 187 | } 188 | } 189 | } 190 | } 191 | }, 192 | "cli": { 193 | "schematicCollections": [ 194 | "@ionic/angular-toolkit" 195 | ] 196 | }, 197 | "schematics": { 198 | "@ionic/angular-toolkit:component": { 199 | "styleext": "scss" 200 | }, 201 | "@ionic/angular-toolkit:page": { 202 | "styleext": "scss" 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MyApp 4 | An awesome Ionic/Cordova app. 5 | Ionic Framework Team 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /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.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 | "hosting": { 3 | "public": "www", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /img/about1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/about1.png -------------------------------------------------------------------------------- /img/about2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/about2.png -------------------------------------------------------------------------------- /img/about3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/about3.png -------------------------------------------------------------------------------- /img/android-aboutdark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-aboutdark.png -------------------------------------------------------------------------------- /img/android-aboutlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-aboutlight.png -------------------------------------------------------------------------------- /img/android-categoriesdark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-categoriesdark.png -------------------------------------------------------------------------------- /img/android-detaildark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-detaildark.png -------------------------------------------------------------------------------- /img/android-detaildark1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-detaildark1.png -------------------------------------------------------------------------------- /img/android-detaillight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-detaillight.png -------------------------------------------------------------------------------- /img/android-favouriteslight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-favouriteslight.png -------------------------------------------------------------------------------- /img/android-menudark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-menudark.png -------------------------------------------------------------------------------- /img/android-menulight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/android-menulight.png -------------------------------------------------------------------------------- /img/categories1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/categories1.png -------------------------------------------------------------------------------- /img/categories2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/categories2.png -------------------------------------------------------------------------------- /img/categories3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/categories3.png -------------------------------------------------------------------------------- /img/dark1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/dark1.png -------------------------------------------------------------------------------- /img/dark2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/dark2.png -------------------------------------------------------------------------------- /img/dark3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/dark3.png -------------------------------------------------------------------------------- /img/favourites1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/favourites1.png -------------------------------------------------------------------------------- /img/favourites2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/favourites2.png -------------------------------------------------------------------------------- /img/favourites3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/favourites3.png -------------------------------------------------------------------------------- /img/news-detail1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/news-detail1.png -------------------------------------------------------------------------------- /img/news-detail2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/news-detail2.png -------------------------------------------------------------------------------- /img/news-detail3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/news-detail3.png -------------------------------------------------------------------------------- /img/news1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/news1.png -------------------------------------------------------------------------------- /img/news2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/news2.png -------------------------------------------------------------------------------- /img/news3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/img/news3.png -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-angular-news-app", 3 | "integrations": { 4 | "cordova": {} 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-angular-news-app", 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 | "prerender": "ng run app:prerender" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^17.0.8", 18 | "@angular/common": "^17.0.8", 19 | "@angular/compiler": "^17.0.8", 20 | "@angular/core": "^17.0.8", 21 | "@angular/forms": "^17.0.8", 22 | "@angular/platform-browser": "^17.0.8", 23 | "@angular/platform-browser-dynamic": "^17.0.8", 24 | "@angular/platform-server": "^17.0.8", 25 | "@angular/router": "^17.0.8", 26 | "@ionic-native/core": "^5.36.0", 27 | "@ionic-native/network": "^5.36.0", 28 | "@ionic-native/splash-screen": "^5.36.0", 29 | "@ionic-native/status-bar": "^5.36.0", 30 | "@ionic/angular": "^7.6.2", 31 | "@ionic/cli": "^7.1.6", 32 | "@ionic/storage-angular": "^4.0.0", 33 | "@ngx-translate/core": "^15.0.0", 34 | "@ngx-translate/http-loader": "^8.0.0", 35 | "cookie-parser": "^1.4.6", 36 | "cordova-plugin-network-information": "^3.0.0", 37 | "cordova-sqlite-storage": "^6.1.0", 38 | "core-js": "^3.35.0", 39 | "dayjs": "^1.11.10", 40 | "rxjs": "^7.8.1", 41 | "tslib": "^2.6.2", 42 | "zone.js": "^0.14.2" 43 | }, 44 | "devDependencies": { 45 | "@angular-devkit/architect": "^0.1700.8", 46 | "@angular-devkit/build-angular": "^17.0.8", 47 | "@angular-devkit/core": "^17.0.8", 48 | "@angular-devkit/schematics": "^17.0.8", 49 | "@angular/cli": "^17.0.8", 50 | "@angular/compiler-cli": "^17.0.8", 51 | "@angular/language-service": "^17.0.8", 52 | "@babel/eslint-parser": "7.23.3", 53 | "@ionic/angular-toolkit": "^10.0.0", 54 | "@types/jasmine": "~5.1.4", 55 | "@types/jasminewd2": "~2.0.13", 56 | "@types/node": "^20.10.6", 57 | "@typescript-eslint/parser": "^6.16.0", 58 | "codelyzer": "^6.0.2", 59 | "eslint": "^8.56.0", 60 | "eslint-plugin-import": "^2.29.1", 61 | "jasmine-core": "~5.1.1", 62 | "jasmine-spec-reporter": "~7.0.0", 63 | "karma": "~6.4.2", 64 | "karma-chrome-launcher": "~3.2.0", 65 | "karma-coverage-istanbul-reporter": "~3.0.3", 66 | "karma-jasmine": "~5.1.0", 67 | "karma-jasmine-html-reporter": "^2.1.0", 68 | "protractor": "^7.0.0", 69 | "ts-node": "~10.9.2", 70 | "typescript": "^5.2.2" 71 | }, 72 | "description": "An Ionic project", 73 | "cordova": { 74 | "plugins": { 75 | "cordova-sqlite-storage": {}, 76 | "cordova-plugin-network-information": {} 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | These are Cordova resources. You can replace icon.png and splash.png and run 2 | `ionic cordova resources` to generate custom icons and splash screens for your 3 | app. See `ionic cordova resources --help` for details. 4 | 5 | Cordova reference documentation: 6 | 7 | - Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html 8 | - Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/ 9 | -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | localhost 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-1024.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-20.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-20@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-20@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-24@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-27.5@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-29.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-29@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-29@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-40@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-44@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-83.5@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-86@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-98@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-2436h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-2436h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-2436h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Landscape-2436h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Landscape@~ipadpro.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Portrait@~ipadpro.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~universal~anyany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default@2x~universal~anyany.png -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/resources/splash.png -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { PreloadAllModules, RouterModule, Routes } from "@angular/router"; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: "", 7 | redirectTo: "/app/tabs/news", 8 | pathMatch: "full", 9 | }, 10 | { 11 | path: "app", 12 | loadChildren: () => 13 | import("./pages/tabs/tabs.module").then((m) => m.TabsPageModule), 14 | }, 15 | { 16 | path: "news-detail", 17 | loadComponent: () => 18 | import("./pages/news-detail/news-detail.page").then((m) => m.NewsDetailPage), 19 | }, 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [ 24 | RouterModule.forRoot(routes, { 25 | preloadingStrategy: PreloadAllModules, 26 | initialNavigation: "enabledBlocking", 27 | }), 28 | ], 29 | exports: [RouterModule], 30 | }) 31 | export class AppRoutingModule {} 32 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ "MENU.title" | translate }} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @for (p of appPages; track p.id ) { 22 | 23 | 24 | @if (language === 'en') { 25 | 26 | {{ p.title.en }} 27 | 28 | } 29 | @else if (language === 'fr') { 30 | 31 | {{ p.title.fr }} 32 | 33 | } 34 | @else if (language ==='sp') { 35 | 36 | {{ p.title.sp }} 37 | 38 | } 39 | 40 | } 41 | 42 | 43 | 44 | 45 | 46 | 47 | {{ "MENU.Label-darkMode" | translate }} 48 | 50 | 51 | 52 | 53 | 54 | {{ "MENU.Label-language" | translate }} 55 | 56 | {{ 57 | "MENU.LangOption-English" | translate 58 | }} 59 | {{ 60 | "MENU.LangOption-Spanish" | translate 61 | }} 62 | {{ 63 | "MENU.LangOption-French" | translate 64 | }} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AppComponent class represents the root component of the application. 3 | * It is responsible for initializing the app, setting up the platform, and handling various functionalities. 4 | * 5 | * Properties: 6 | * - platform: Platform instance for accessing platform-specific functionalities. 7 | * - router: Router instance for navigating between different routes. 8 | * - splashScreen: SplashScreen instance for controlling the splash screen. 9 | * - statusBar: StatusBar instance for controlling the status bar. 10 | * - themeService: ThemeService instance for managing the app's theme. 11 | * - networkService: NetworkService instance for handling network-related functionalities. 12 | * - toastController: ToastController instance for displaying toast messages. 13 | * - languageService: LanguageService instance for managing the app's language. 14 | * - storageService: StorageService instance for accessing and manipulating stored data. 15 | * - darkMode: A boolean flag indicating whether the app is in dark mode or not. 16 | * - language: The selected language for the app. 17 | * - menuCtrl: MenuController instance for controlling the app's menu. 18 | * - appPages: An array of app pages. 19 | * 20 | * Methods: 21 | * - initializeApp(): Initializes the app by setting up the platform, status bar, splash screen, language, and dark mode. 22 | * - darkStartMode(): Sets the app's theme based on the stored dark mode value. 23 | * - languageChange(): Changes the app's language based on the selected language. 24 | * - closeMenu(event: any): Closes the app's menu if it is open. 25 | */ 26 | 27 | import { APP_PAGES } from "../assets/pages-data"; 28 | // angular & ionic/angular node modules 29 | import { Component, ViewEncapsulation, inject } from "@angular/core"; 30 | import { Router } from "@angular/router"; 31 | import { MenuController, Platform, ToastController } from "@ionic/angular"; 32 | 33 | // ionic-native & ngx node modules 34 | import { SplashScreen } from "@ionic-native/splash-screen/ngx"; 35 | import { StatusBar } from "@ionic-native/status-bar/ngx"; 36 | 37 | // Services 38 | import { NetworkService } from "./providers/network.service"; 39 | import { ThemeService } from "./providers/theme.service"; 40 | import { LanguageService } from "./providers/language.service"; 41 | import { StorageService } from "./providers/storage.service"; 42 | 43 | @Component({ 44 | selector: "app-root", 45 | templateUrl: "app.component.html", 46 | styleUrls: ["app.scss"], 47 | encapsulation: ViewEncapsulation.None, 48 | }) 49 | export class AppComponent { 50 | private platform = inject(Platform); 51 | private router = inject(Router); 52 | private splashScreen = inject(SplashScreen); 53 | private statusBar = inject(StatusBar); 54 | public themeService = inject(ThemeService); 55 | public networkService = inject(NetworkService); 56 | public toastController = inject(ToastController); 57 | private languageService = inject(LanguageService); 58 | private storageService = inject(StorageService); 59 | 60 | public darkMode: boolean; 61 | public language: string = this.languageService.selected; 62 | private menuCtrl: MenuController; 63 | public appPages = APP_PAGES; 64 | 65 | constructor() { 66 | this.initializeApp(); 67 | } 68 | 69 | initializeApp() { 70 | this.platform.ready().then(() => { 71 | this.statusBar.styleDefault(); 72 | this.splashScreen.hide(); 73 | this.languageService.setInitialAppLanguage(); 74 | this.darkStartMode(); 75 | }); 76 | } 77 | 78 | async darkStartMode() { 79 | this.storageService.getStoredData("dark-theme").then((val) => { 80 | if (val !== null && val !== undefined) { 81 | this.darkMode = JSON.parse(val); 82 | this.darkMode === true 83 | ? this.themeService.enableDark() 84 | : this.themeService.enableLight(); 85 | } else { 86 | // Handle null or undefined stored data 87 | this.themeService.enableLight(); 88 | } 89 | }); 90 | } 91 | 92 | languageChange() { 93 | this.languageService.setLanguage(this.language); 94 | } 95 | 96 | async closeMenu(event: any) { 97 | if (this.menuCtrl.isOpen()) { 98 | await this.menuCtrl.close(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | // Core imports 2 | import { HttpClient, HttpClientModule } from "@angular/common/http"; 3 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; 4 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 5 | import { BrowserModule } from "@angular/platform-browser"; 6 | import { RouteReuseStrategy } from "@angular/router"; 7 | import { IonicModule, IonicRouteStrategy } from "@ionic/angular"; 8 | 9 | // Components and modules 10 | import { AppComponent } from "./app.component"; 11 | import { AppRoutingModule } from "./app-routing.module"; 12 | 13 | // Third party imports 14 | import { Network } from "@ionic-native/network/ngx"; 15 | import { SplashScreen } from "@ionic-native/splash-screen/ngx"; 16 | import { StatusBar } from "@ionic-native/status-bar/ngx"; 17 | import { Storage } from "@ionic/storage-angular"; 18 | import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; 19 | import { TranslateHttpLoader } from "@ngx-translate/http-loader"; 20 | 21 | // exported translations loader function that fetches JSON files from the assets folder 22 | export function createTranslateLoader(http: HttpClient) { 23 | return new TranslateHttpLoader(http, "assets/i18n/", ".json"); 24 | } 25 | 26 | @NgModule({ 27 | declarations: [AppComponent], 28 | imports: [ 29 | BrowserModule, 30 | FormsModule, 31 | ReactiveFormsModule, 32 | IonicModule.forRoot(), 33 | AppRoutingModule, 34 | HttpClientModule, 35 | TranslateModule.forRoot({ 36 | loader: { 37 | provide: TranslateLoader, 38 | useFactory: createTranslateLoader, 39 | deps: [HttpClient], 40 | }, 41 | }), 42 | ], 43 | providers: [ 44 | Storage, 45 | Network, 46 | StatusBar, 47 | SplashScreen, 48 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 49 | ], 50 | bootstrap: [AppComponent], 51 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 52 | }) 53 | export class AppModule {} 54 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--ion-color-primary); 2 | $color_2: var(--ion-color-success); 3 | $color_3: var(--ion-color-secondary); 4 | $color_4: var(--ion-color-tertiary); 5 | 6 | .active { 7 | color: $color_1; 8 | ion-icon { 9 | color: $color_3; 10 | } 11 | } 12 | 13 | .iconGlobe { 14 | color: $color_2; 15 | } 16 | 17 | ion-toggle { 18 | color: $color_3 !important; 19 | } 20 | 21 | .icon { 22 | vertical-align: middle; 23 | color: $color_4; 24 | } 25 | 26 | .custom-toast { 27 | background: white !important; 28 | opacity: 0.5; 29 | box-shadow: 3px 3px 10px 0 rgba(0, 0, 0, 0.2); 30 | color: $color_2; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/components/article-list/article-list.component.html: -------------------------------------------------------------------------------- 1 | @if (article.urlToImage) { 2 | 3 | 4 | 5 | 6 | {{ article.title }} 11 | 12 | 13 | 14 | 15 | 16 | {{ article.title | titleConvert }} 17 | 18 | {{ article.publishedAt | dateConvert }} 19 | 20 | 21 | 22 | } -------------------------------------------------------------------------------- /src/app/components/article-list/article-list.component.scss: -------------------------------------------------------------------------------- 1 | ion-label { 2 | color: var(--ion-color-light-contrast); 3 | font-size: 14px; 4 | } 5 | 6 | .image-col { 7 | padding: 0; 8 | 9 | ion-thumbnail { 10 | width: 100%; 11 | max-height: 89.2px; 12 | min-height: 68.4px; 13 | height: 100%; 14 | border-right-style: solid; 15 | border-right-width: 2px; 16 | border-right-color: var(--ion-color-tertiary); 17 | 18 | .article-card-image { 19 | width: 140px; 20 | height: 224px; 21 | } 22 | } 23 | } 24 | 25 | .text-col { 26 | background-color: var(--ion-color-light); 27 | } 28 | 29 | .small-text { 30 | color: var(--ion-color-secondary); 31 | white-space: nowrap; 32 | margin-left: 4px; 33 | } 34 | 35 | .news-text { 36 | display: flex; 37 | align-items: center; 38 | margin-left: 4px; 39 | color: var(ion-color-primary-contrast); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/components/article-list/article-list.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ArticleListComponent displays a list of Article data. 3 | * 4 | * @Component decorator defines this as an Angular component. 5 | * - selector: Custom HTML element to use this component. 6 | * - templateUrl: Points to the component's HTML template. 7 | * - styleUrls: Points to the component's CSS styles. 8 | * 9 | * @Input() article: Binds an Article object to display in the template. 10 | */ 11 | import { Component, Input } from "@angular/core"; 12 | import { Article } from "../../interfaces/interfaces"; 13 | import { TitleConvertPipe } from "../../pipes/title-convert.pipe"; 14 | import { DateConvertPipe } from "../../pipes/date-convert.pipe"; 15 | import { IonicModule } from "@ionic/angular"; 16 | 17 | @Component({ 18 | selector: "app-article-list", 19 | templateUrl: "./article-list.component.html", 20 | styleUrls: ["./article-list.component.scss"], 21 | standalone: true, 22 | imports: [ 23 | IonicModule, 24 | DateConvertPipe, 25 | TitleConvertPipe, 26 | ], 27 | }) 28 | export class ArticleListComponent { 29 | @Input() article: Article | null; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/page-refresh/page-refresh.component.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/components/page-refresh/page-refresh.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/app/components/page-refresh/page-refresh.component.scss -------------------------------------------------------------------------------- /src/app/components/page-refresh/page-refresh.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { IonicModule } from "@ionic/angular"; 3 | 4 | @Component({ 5 | selector: "app-page-refresh", 6 | templateUrl: "./page-refresh.component.html", 7 | styleUrls: ["./page-refresh.component.scss"], 8 | standalone: true, 9 | imports: [IonicModule], 10 | }) 11 | export class PageRefreshComponent {} 12 | -------------------------------------------------------------------------------- /src/app/components/progress-bar/progress-bar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ "PROGRESS_BAR.title" | translate }} 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/components/progress-bar/progress-bar.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/app/components/progress-bar/progress-bar.component.scss -------------------------------------------------------------------------------- /src/app/components/progress-bar/progress-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { TranslateModule } from "@ngx-translate/core"; 3 | import { IonicModule } from "@ionic/angular"; 4 | 5 | @Component({ 6 | selector: "app-progress-bar", 7 | templateUrl: "./progress-bar.component.html", 8 | styleUrls: ["./progress-bar.component.scss"], 9 | standalone: true, 10 | imports: [IonicModule, TranslateModule], 11 | }) 12 | export class ProgressBarComponent {} 13 | -------------------------------------------------------------------------------- /src/app/components/svgs/news-svg/news-svg.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-news-svg", 5 | templateUrl: "../../../../../src/assets/svgs/newspaper.svg", 6 | styleUrls: ["../svg.component.scss"], 7 | standalone: true, 8 | }) 9 | export class NewsSvgComponent {} 10 | -------------------------------------------------------------------------------- /src/app/components/svgs/svg.component.scss: -------------------------------------------------------------------------------- 1 | svg { 2 | width: 80px; 3 | height: 80px; 4 | fill: white; 5 | background: inherit; 6 | background-color: inherit; 7 | padding: 0 8px 0 0; 8 | } 9 | 10 | img { 11 | width: inherit; 12 | height: inherit; 13 | } -------------------------------------------------------------------------------- /src/app/interfaces/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interfaces defining the response formats from the IP location API 3 | * and News API used in this application. 4 | */ 5 | // format of response from IP location API 6 | export interface IpLocationResponse { 7 | ip: string; 8 | city: string; 9 | region: string; 10 | region_code: string; 11 | country: string; 12 | country_name: string; 13 | continent_code: string; 14 | in_eu: boolean; 15 | postal: number; 16 | latitude: number; 17 | longitude: number; 18 | timezone: string; 19 | utc_offset: number; 20 | country_calling_code: number; 21 | currency: string; 22 | languages: string; 23 | asn: string; 24 | org: string; 25 | } 26 | 27 | // format of news API response arrays 28 | export interface SourcesResponse { 29 | status: "ok"; 30 | sources: Source[]; 31 | } 32 | 33 | export interface Source { 34 | id: string; 35 | name: string; 36 | description: string; 37 | url: string; 38 | category: string; 39 | language: string; 40 | country: string; 41 | } 42 | 43 | export interface NewsApiResponse { 44 | status: string; 45 | totalResults: number; 46 | articles: Article[]; 47 | } 48 | 49 | // format of each Article array in the API response 50 | export interface Article { 51 | source: Source; 52 | author?: string; 53 | title: string; 54 | description: string; 55 | url: string; 56 | urlToImage: string; 57 | publishedAt: string; 58 | content?: string; 59 | } 60 | -------------------------------------------------------------------------------- /src/app/pages/about-popover/about-popover.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | Contact the Author 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Author Website 20 | 21 | 22 | 23 | 24 | 25 | 26 | App Repository 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/pages/about-popover/about-popover.scss: -------------------------------------------------------------------------------- 1 | ion-label { 2 | display: flex !important; 3 | align-items: center; 4 | } 5 | 6 | ion-label ion-icon { 7 | margin-right: 8px; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/pages/about-popover/about-popover.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PopoverPage class represents a component that displays a popover with various options. 3 | * It is used in an Ionic Angular application. 4 | * 5 | * Properties: 6 | * - urlHome: string - The URL for the home page. 7 | * - urlContact: string - The URL for the contact page. 8 | * - urlGithub: string - The URL for the GitHub repository. 9 | * 10 | * Methods: 11 | * - openContactForm(): Promise - Navigates to the contact page and dismisses the popover. 12 | * - openUrl(url: string): void - Opens the given URL in a new browser tab and dismisses the popover. 13 | */ 14 | import { Component, inject } from "@angular/core"; 15 | import { PopoverController } from "@ionic/angular"; 16 | import { Router } from "@angular/router"; 17 | import { IonicModule } from "@ionic/angular"; 18 | 19 | @Component({ 20 | templateUrl: "./about-popover.html", 21 | styleUrls: ["./about-popover.scss"], 22 | standalone: true, 23 | imports: [IonicModule], 24 | }) 25 | export class PopoverPage { 26 | private router = inject(Router); 27 | private popoverCtrl = inject(PopoverController); 28 | 29 | public urlHome = "https://andrewbateman.org"; 30 | public urlContact = "https://andrewbateman.org/contact"; 31 | public urlGithub = "https://github.com/AndrewJBateman/ionic-angular-news-app"; 32 | 33 | async openContactForm() { 34 | await this.router.navigate(["app/tabs/contact"]); 35 | await this.popoverCtrl.dismiss(); 36 | } 37 | 38 | openUrl(url: string) { 39 | window.open(url, "_blank"); 40 | this.popoverCtrl.dismiss(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/pages/about/about.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 | 31 |
    32 | {{ 'ABOUT.photo-credit' | translate }} 37 |
    38 | {{ 'ABOUT.photo-credit' | translate }} 39 |
    40 |
    41 | 42 | 43 | {{ 'ABOUT.card-title' | translate }} 51 | 52 | 53 |

    54 | {{ 'ABOUT.para1-part1' | translate }}News API, {{ 'ABOUT.para1-part2' | translate }} 61 |

    62 |

    {{ 'ABOUT.para2' | translate }}

    63 |

    {{ 'ABOUT.para3' | translate }}

    64 |
    65 |
    66 |
    67 |
    68 |
    69 |
    70 | -------------------------------------------------------------------------------- /src/app/pages/about/about.page.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--ion-color-secondary); 2 | $color_2: var(--ion-color-light-contrast); 3 | $color_3: var(--ion-color-secondary-shade); 4 | $color_4: white; 5 | $color_5: var(--ion-color-tertiary); 6 | $background-color_1: transparent; 7 | $background-color_2: var(--ion-color-light); 8 | $border-bottom-color_1: var(--ion-color-tertiary); 9 | 10 | /* Bottom right text */ 11 | .about-header { 12 | img { 13 | width: 100%; 14 | height: 100%; 15 | max-height: 30vh; 16 | object-fit: cover; 17 | } 18 | } 19 | .title-icon { 20 | vertical-align: middle; 21 | color: $color_1; 22 | padding-right: 4px; 23 | } 24 | p { 25 | width: 100%; 26 | text-align: justify; 27 | margin: auto 0 10px; 28 | color: $color_2; 29 | } 30 | a { 31 | &:link { 32 | color: $color_1; 33 | background-color: $background-color_1; 34 | text-decoration: none; 35 | } 36 | &:visited { 37 | color: $color_3; 38 | background-color: $background-color_1; 39 | text-decoration: underline; 40 | } 41 | &:hover { 42 | color: $color_3; 43 | background-color: $background-color_1; 44 | text-decoration: underline; 45 | } 46 | &:active { 47 | color: $color_3; 48 | background-color: $background-color_1; 49 | text-decoration: underline; 50 | } 51 | } 52 | .image-div { 53 | position: relative; 54 | text-align: center; 55 | color: $color_4; 56 | } 57 | .mat-card-image { 58 | border-bottom-style: solid; 59 | border-bottom-width: 4px; 60 | border-bottom-color: $border-bottom-color_1; 61 | } 62 | .photo-credit { 63 | position: absolute; 64 | bottom: 10px; 65 | right: 10px; 66 | background: rgb(0, 0, 0); 67 | background: rgba(0, 0, 0, 0.55); 68 | color: $color_4; 69 | padding-left: 4px; 70 | padding-right: 4px; 71 | } 72 | .icon { 73 | vertical-align: middle; 74 | color: $color_5; 75 | } 76 | .about-card { 77 | background-color: $background-color_2; 78 | } 79 | -------------------------------------------------------------------------------- /src/app/pages/about/about.page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the AboutPage class. 3 | * 4 | * @class 5 | * @description The AboutPage class is responsible for creating and presenting a popover component when a mouse event occurs. 6 | * If an error occurs during the creation or presentation of the popover, an error toast is displayed and the error is thrown. 7 | */ 8 | import { Component, inject } from "@angular/core"; 9 | import { PopoverController } from "@ionic/angular"; 10 | import { TranslateModule } from "@ngx-translate/core"; 11 | import { IonicModule } from "@ionic/angular"; 12 | 13 | import { PopoverPage } from "../about-popover/about-popover"; 14 | import { ToastService } from "../../providers/toast.service"; 15 | 16 | @Component({ 17 | selector: "app-about", 18 | templateUrl: "./about.page.html", 19 | styleUrls: ["./about.page.scss"], 20 | standalone: true, 21 | imports: [PopoverPage, TranslateModule, IonicModule], 22 | providers: [ToastService], 23 | }) 24 | export class AboutPage { 25 | private popoverCtrl = inject(PopoverController); 26 | private toastService = inject(ToastService); 27 | 28 | /** 29 | * Presents the popover component when a mouse event occurs. 30 | * 31 | * @param {MouseEvent} event - The mouse event that triggers the popover creation and presentation. 32 | * @returns {Promise} - A promise that resolves when the popover is presented successfully. 33 | * @throws {Error} - If an error occurs during the creation or presentation of the popover. 34 | */ 35 | async presentPopover(event: MouseEvent): Promise { 36 | try { 37 | const popover = await this.popoverCtrl.create({ 38 | component: PopoverPage, 39 | event: event, 40 | }); 41 | await popover.present(); 42 | } catch (error) { 43 | this.toastService.presentErrorToast( 44 | `An error occurred: "${error.message}". Please try again later.` 45 | ); 46 | throw error(error); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/pages/categories/categories.page.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | @for (category of categories; track category) { 21 | 22 | {{ category }} 23 | 24 | 25 | } 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | @if (newsData === undefined) { 39 | 40 | } 41 | 42 | 43 |
    44 | @for (article of newsData?.articles; track trackByPublishedDate) { 45 |
    46 | 47 |
    48 | } 49 |
    50 |
    51 |
    52 |
    53 |
    -------------------------------------------------------------------------------- /src/app/pages/categories/categories.page.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--ion-color-tertiary); 2 | 3 | .category-label { 4 | color: $color_1; 5 | vertical-align: middle; 6 | } 7 | .icon { 8 | vertical-align: middle; 9 | color: $color_1; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/pages/categories/categories.page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the CategoriesPage component. 3 | * This component is responsible for displaying news articles based on selected categories. 4 | * It uses the NewsApiService to fetch news data and the NetworkService to handle network-related operations. 5 | * The component also includes methods for changing the category, loading category news, navigating to news detail, and refreshing the page. 6 | * @class 7 | */ 8 | import { Component, OnInit, inject, HostListener } from "@angular/core"; 9 | import { CommonModule } from "@angular/common"; 10 | import { FormsModule } from "@angular/forms"; 11 | import { Router } from "@angular/router"; 12 | import { Observable } from "rxjs"; 13 | import { IonicModule } from "@ionic/angular"; 14 | import { TranslateModule } from "@ngx-translate/core"; 15 | 16 | import { NewsApiService } from "../../providers/news-api.service"; 17 | import { NetworkService } from "./../../providers/network.service"; 18 | import { ToastService } from "../../providers/toast.service"; 19 | import { Article, NewsApiResponse } from "../../interfaces/interfaces"; 20 | import { PipesModule } from "../../pipes/pipes.module"; 21 | 22 | import { ArticleListComponent } from "../../components/article-list/article-list.component"; 23 | import { ProgressBarComponent } from "../../components/progress-bar/progress-bar.component"; 24 | import { PageRefreshComponent } from "../../components/page-refresh/page-refresh.component"; 25 | 26 | @Component({ 27 | selector: "app-categories", 28 | templateUrl: "./categories.page.html", 29 | styleUrls: ["./categories.page.scss"], 30 | standalone: true, 31 | imports: [ 32 | ArticleListComponent, 33 | CommonModule, 34 | FormsModule, 35 | IonicModule, 36 | PageRefreshComponent, 37 | PipesModule, 38 | ProgressBarComponent, 39 | TranslateModule, 40 | ], 41 | }) 42 | export class CategoriesPage implements OnInit { 43 | private router = inject(Router); 44 | private newsService = inject(NewsApiService); 45 | public networkService = inject(NetworkService); 46 | private toastService = inject(ToastService); 47 | 48 | categories = [ 49 | "general", 50 | "technology", 51 | "business", 52 | "entertainment", 53 | "health", 54 | "science", 55 | "sports", 56 | ]; 57 | newsArticles: Article[] = []; 58 | newsData: NewsApiResponse; 59 | category: string; 60 | @HostListener('window:keydown.enter', ['$event']) 61 | 62 | onRefresh(event: KeyboardEvent | MouseEvent | TouchEvent): void { 63 | this.networkService.refreshPage(event); 64 | } 65 | 66 | ngOnInit() { 67 | this.category = "general"; 68 | this.loadCategoryNews(this.category); 69 | } 70 | 71 | changeCategory(event: any) { 72 | const newNewsArticles: Article[] = []; 73 | this.loadCategoryNews(event.detail.value); 74 | this.newsArticles = newNewsArticles; 75 | } 76 | 77 | loadCategoryNews(category: string) { 78 | const url = `top-headlines?category=${encodeURIComponent( 79 | category 80 | )}&country=us`; 81 | this.newsService.getNews(url).subscribe( 82 | (data: NewsApiResponse) => { 83 | this.newsData = data; 84 | }, 85 | (error) => { 86 | this.toastService.presentErrorToast( 87 | `An error occurred: "${error.message}". Please try again later.` 88 | ); 89 | throw error(error); 90 | } 91 | ); 92 | } 93 | 94 | onGoToNewsDetail(article: Article) { 95 | this.newsService.getNewsDetail(article); 96 | } 97 | 98 | public trackByPublishedDate(index: number, article: Article): string { 99 | return article ? article.publishedAt : null; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/pages/favourites/favourites-popover/favourites-popover.ts: -------------------------------------------------------------------------------- 1 | import { StorageService } from './../../../providers/storage.service'; 2 | import { PopoverController, IonicModule } from "@ionic/angular"; 3 | import { Component, ChangeDetectorRef, inject, OnInit } from "@angular/core"; 4 | 5 | @Component({ 6 | template: ` 7 | 8 | 9 | 10 | 11 | Clear favourites 12 | 13 | 14 | 15 | `, 16 | standalone: true, 17 | imports: [IonicModule], 18 | }) 19 | export class PopoverPage implements OnInit { 20 | private popoverController = inject(PopoverController); 21 | private storageService = inject(StorageService); 22 | 23 | constructor(private changeDetectorRef: ChangeDetectorRef) {} 24 | 25 | ngOnInit() { 26 | // initialization logic 27 | } 28 | 29 | clearFavourites() { 30 | this.storageService.deleteStoredFavourites(); 31 | this.popoverController.dismiss(); 32 | this.changeDetectorRef.detectChanges(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/pages/favourites/favourites.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | @if (storageService.news === undefined) { 25 | 26 | } 27 | 28 | 29 | @if (storageService.news.length === 0) { 30 | 31 |
    32 | 33 |
    34 | 35 | 36 |

    37 | {{ 'FAVOURITES.notice' | translate }} 38 |

    39 |
    40 |
    41 | } 42 | 43 | 44 | @if (storageService.news.length > 0) { 45 |
    46 | 47 | @for (article of storageService.news; track trackByPublishedDate) { 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | } 58 | 59 | 60 |
    61 | } 62 |
    63 |
    64 |
    65 |
    -------------------------------------------------------------------------------- /src/app/pages/favourites/favourites.page.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--ion-color-light-contrast); 2 | $color_2: white; 3 | $color_3: var(--ion-color-tertiary); 4 | $color_4: var(--ion-color-secondary); 5 | 6 | h2 { 7 | color: $color_1; 8 | } 9 | .no-favourites-card { 10 | position: relative; 11 | margin-top: 80px; 12 | text-align: center; 13 | color: $color_2; 14 | } 15 | .icon { 16 | vertical-align: middle; 17 | color: $color_3; 18 | } 19 | .card-svg { 20 | vertical-align: middle; 21 | color: $color_4; 22 | } -------------------------------------------------------------------------------- /src/app/pages/favourites/favourites.page.ts: -------------------------------------------------------------------------------- 1 | import { IonItemSliding, LoadingController, IonicModule } from "@ionic/angular"; 2 | import { Component, inject, ChangeDetectorRef } from "@angular/core"; 3 | import { CommonModule } from "@angular/common"; 4 | import { FormsModule } from "@angular/forms"; 5 | import { PopoverController } from "@ionic/angular"; 6 | import { TranslateModule } from "@ngx-translate/core"; 7 | 8 | import { StorageService } from "./../../providers/storage.service"; 9 | import { NewsApiService } from "./../../providers/news-api.service"; 10 | import { NetworkService } from "./../../providers/network.service"; 11 | import { Article } from "../../interfaces/interfaces"; 12 | 13 | import { PopoverPage } from "./favourites-popover/favourites-popover"; 14 | import { ArticleListComponent } from "../../components/article-list/article-list.component"; 15 | import { NewsSvgComponent } from "../../components/svgs/news-svg/news-svg.component"; 16 | import { ProgressBarComponent } from "../../components/progress-bar/progress-bar.component"; 17 | import { PageRefreshComponent } from "../../components/page-refresh/page-refresh.component"; 18 | 19 | @Component({ 20 | selector: "app-favourites", 21 | templateUrl: "./favourites.page.html", 22 | styleUrls: ["./favourites.page.scss"], 23 | standalone: true, 24 | imports: [ 25 | CommonModule, 26 | FormsModule, 27 | IonicModule, 28 | PageRefreshComponent, 29 | ProgressBarComponent, 30 | NewsSvgComponent, 31 | ArticleListComponent, 32 | TranslateModule, 33 | ], 34 | }) 35 | export class FavouritesPage { 36 | public storageService = inject(StorageService); 37 | public networkService = inject(NetworkService); 38 | public newsService = inject(NewsApiService); 39 | private loadingController = inject(LoadingController); 40 | private popoverController = inject(PopoverController); 41 | 42 | sliderOptions = { 43 | allowSlidePrev: false, 44 | allowSlideNext: false, 45 | }; 46 | 47 | private loadingElement: any; 48 | 49 | /** 50 | * Presents the popover component. 51 | * @param event The event that triggered the popover. 52 | */ 53 | presentPopover(event: KeyboardEvent | MouseEvent | TouchEvent): Promise { 54 | return new Promise(async (resolve) => { 55 | const popover = await this.popoverController.create({ 56 | component: PopoverPage, 57 | event: event, 58 | }); 59 | await popover.present(); 60 | resolve(); 61 | }); 62 | } 63 | 64 | /** 65 | * Removes the favourite article and closes the sliding item. 66 | * @param article The article to be removed. 67 | * @param slidingItem The sliding item to be closed. 68 | */ 69 | onRemoveFavourite(article: Article, slidingItem: IonItemSliding) { 70 | slidingItem.close(); 71 | this.loadingElement.present(); 72 | this.storageService.removeFromFavourites(article); 73 | this.loadingElement.dismiss(); 74 | } 75 | 76 | public trackByPublishedDate(index: number, article: Article): string { 77 | return article ? article.publishedAt : null; 78 | } 79 | } -------------------------------------------------------------------------------- /src/app/pages/news-detail/news-detail.page.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | @if (article.source.name) { 29 | {{ 'NEWS-DETAIL.title' | translate }} 30 | {{ article.source.name }} 31 | } 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {{ article.title | titleNosource }} 46 | @if (article.content) { 47 | {{ appendString(article.content) }}
    {{ 48 | article.publishedAt | dateConvert }} 49 |
    50 | } 51 |
    52 |
    53 |
    54 | @if (article.author) { 55 | 56 | 57 | {{ 58 | 'NEWS-DETAIL.author' | translate }}: {{ article.author }} 59 | 60 | 61 | } 62 |
    63 |
    64 |
    65 |
    66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 |
    77 | @if (!storageService.isFavourite(article);) { 78 | 79 | 80 | 83 | 84 | } 85 | 86 | @if (storageService.isFavourite(article);) { 87 | 88 | 89 | 92 | 93 | } 94 |
    95 |
    96 |
    97 | ''' -------------------------------------------------------------------------------- /src/app/pages/news-detail/news-detail.page.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--ion-color-secondary); 2 | $color_2: var(--ion-color-tertiary); 3 | $border-bottom-color_1: var(--ion-color-tertiary); 4 | 5 | ion-icon { 6 | color: $color_1; 7 | } 8 | ion-img { 9 | width: 100%; 10 | } 11 | .small-text { 12 | color: $color_1; 13 | } 14 | .footer-text { 15 | font-size: 12px; 16 | } 17 | ion-list { 18 | padding: 16px; 19 | } 20 | ion-list-header { 21 | padding: 0 0 8px; 22 | font-size: 16px; 23 | } 24 | ion-button { 25 | color: $color_1; 26 | padding-left: 6px; 27 | } 28 | .detail-img { 29 | border-bottom-style: solid; 30 | border-bottom-width: 4px; 31 | border-bottom-color: $border-bottom-color_1; 32 | } 33 | .source-text { 34 | color: $color_2; 35 | } 36 | .author-text { 37 | color: $color_1; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/pages/news-detail/news-detail.page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The `NewsDetailPage` class is responsible for displaying the details of a news article. 3 | */ 4 | import { Component, OnInit } from "@angular/core"; 5 | import { Router } from "@angular/router"; 6 | import { 7 | AlertController, 8 | LoadingController, 9 | ToastController, 10 | IonicModule, 11 | } from "@ionic/angular"; 12 | import { CommonModule } from "@angular/common"; 13 | import { FormsModule } from "@angular/forms"; 14 | 15 | import { NewsApiService } from "src/app/providers/news-api.service"; 16 | import { StorageService } from "src/app/providers/storage.service"; 17 | import { Article } from "./../../interfaces/interfaces"; 18 | import { TitleNosourcePipe } from "../../pipes/title-nosource.pipe"; 19 | import { DateConvertPipe } from "../../pipes/date-convert.pipe"; 20 | import { TranslateModule } from "@ngx-translate/core"; 21 | 22 | @Component({ 23 | selector: "app-news-detail", 24 | templateUrl: "./news-detail.page.html", 25 | styleUrls: ["./news-detail.page.scss"], 26 | standalone: true, 27 | imports: [ 28 | CommonModule, 29 | FormsModule, 30 | IonicModule, 31 | TranslateModule, 32 | DateConvertPipe, 33 | TitleNosourcePipe, 34 | ], 35 | }) 36 | export class NewsDetailPage implements OnInit { 37 | article: Article; 38 | 39 | constructor( 40 | public newsService: NewsApiService, 41 | public alertCtrl: AlertController, 42 | public loadingCtrl: LoadingController, 43 | public toastCtrl: ToastController, 44 | public storageService: StorageService, 45 | public router: Router 46 | ) {} 47 | 48 | /** 49 | * Initializes the component and sets the `article` property to the current article from the `NewsApiService`. 50 | */ 51 | ngOnInit() { 52 | this.article = this.newsService.currentArticle; 53 | } 54 | 55 | /** 56 | * Appends "(For more 'Visit Website' below)" to the given content string. 57 | * 58 | * @param content - The content string to modify. 59 | * @returns The modified content string with "(For more 'Visit Website' below)" appended to it. 60 | */ 61 | appendString(content: string) { 62 | try { 63 | let result = content 64 | .split("[")[0] 65 | .concat(`(For more 'Visit Website' below)`); 66 | return result; 67 | } catch (err) { 68 | throw err; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/pages/news/news.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | @for (item of storedSources; track trackById) { 16 | 17 | {{ item.name }} 18 | 19 | } 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | @if (newsData === undefined) { 31 | 32 | } 33 | 34 | 35 | @if (!sourceChosen && newsData) { 36 |
    37 | @for (article of newsData; track trackByPublishedDate) { 38 |
    40 | 41 |
    42 | } 43 |
    44 | } 45 | 46 | 47 | @if (sourceChosen) { 48 |
    49 | @for (article of newsData; track trackByPublishedDate) { 50 |
    51 | 52 |
    53 | } 54 |
    55 | } 56 |
    57 |
    58 |
    59 |
    -------------------------------------------------------------------------------- /src/app/pages/news/news.page.scss: -------------------------------------------------------------------------------- 1 | $color_1: var(--ion-color-tertiary); 2 | 3 | .source-label { 4 | color: $color_1; 5 | vertical-align: middle; 6 | } 7 | .icon { 8 | vertical-align: middle; 9 | color: $color_1; 10 | } 11 | .news-article-list { 12 | padding: 0 !important; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/news/news.page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The NewsPage class is responsible for displaying news articles. It interacts with various services such as NewsApiService, StorageService, and NetworkService to fetch and store data, handle network connectivity, and display toast messages. 3 | * 4 | * Inputs: 5 | * - toastController: An instance of the ToastController class from the Ionic framework. 6 | * - platform: An instance of the Platform class from the Ionic framework. 7 | * - newsService: An instance of the NewsApiService class. 8 | * - storageService: An instance of the StorageService class. 9 | * - networkService: An instance of the NetworkService class. 10 | * - loadingCtrl: An instance of the LoadingController class from the Ionic framework. 11 | * - alertCtrl: An instance of the AlertController class from the Ionic framework. 12 | * 13 | * Outputs: 14 | * - The newsData property is updated with the fetched news articles. 15 | * - Toast messages are displayed to indicate success or failure. 16 | * - The sources property is updated with the fetched news sources. 17 | * - The sourceChosen property is set to true to indicate that a source has been selected. 18 | */ 19 | import { Component, OnInit } from "@angular/core"; 20 | import { CommonModule } from "@angular/common"; 21 | import { FormsModule } from "@angular/forms"; 22 | import { 23 | LoadingController, 24 | ToastController, 25 | Platform, 26 | IonicModule, 27 | } from "@ionic/angular"; 28 | import { AlertController } from "@ionic/angular"; 29 | 30 | import { NewsApiService } from "../../providers/news-api.service"; 31 | import { StorageService } from "../../providers/storage.service"; 32 | import { NetworkService } from "../../providers/network.service"; 33 | 34 | import { 35 | Article, 36 | SourcesResponse, 37 | NewsApiResponse, 38 | } from "../../interfaces/interfaces"; 39 | import { TranslateModule } from "@ngx-translate/core"; 40 | import { ArticleListComponent } from "../../components/article-list/article-list.component"; 41 | import { ProgressBarComponent } from "../../components/progress-bar/progress-bar.component"; 42 | import { PageRefreshComponent } from "../../components/page-refresh/page-refresh.component"; 43 | import { PipesModule } from "src/app/pipes/pipes.module"; 44 | 45 | @Component({ 46 | selector: "app-news", 47 | templateUrl: "./news.page.html", 48 | styleUrls: ["./news.page.scss"], 49 | standalone: true, 50 | imports: [ 51 | CommonModule, 52 | FormsModule, 53 | IonicModule, 54 | TranslateModule, 55 | PipesModule, 56 | PageRefreshComponent, 57 | ProgressBarComponent, 58 | ArticleListComponent 59 | ], 60 | providers: [NetworkService], 61 | }) 62 | export class NewsPage implements OnInit { 63 | newsData: Article[]; 64 | sources = []; 65 | defaultSource = "CNN"; 66 | defaultCountry = "us"; 67 | isConnected = true; 68 | sourceChosen = false; 69 | storedSources: any; 70 | storedData: any; 71 | storedselectedNews: any; 72 | selectedNews: any; 73 | selectedLanguage: "string"; 74 | 75 | constructor( 76 | private toastController: ToastController, 77 | private platform: Platform, 78 | public newsService: NewsApiService, 79 | private storageService: StorageService, 80 | public networkService: NetworkService, 81 | public loadingCtrl: LoadingController, 82 | public alertCtrl: AlertController 83 | ) {} 84 | 85 | ngOnInit() { 86 | this.getSources(); 87 | this.getStoredSources(); 88 | this.getCountryNews(); 89 | } 90 | 91 | async getCountryNews(): Promise { 92 | await this.platform.ready(); 93 | try { 94 | const data: NewsApiResponse = await this.newsService 95 | .getNews(`top-headlines?country=${this.defaultCountry}`) 96 | .toPromise(); 97 | this.newsData = data.articles; 98 | } catch (error) { 99 | this.presentErrorToast( 100 | `An error occurred: "${error.message}". Please try again later.` 101 | ); 102 | } 103 | } 104 | 105 | async getStoredSources(): Promise { 106 | const val = await this.storageService.getStoredData("storedSources"); 107 | this.storedSources = JSON.parse(val); 108 | } 109 | 110 | async presentErrorToast(message: string): Promise { 111 | const toast = await this.toastController.create({ 112 | message: message, 113 | duration: 2000, 114 | position: "middle", 115 | color: "danger", 116 | cssClass: "custom-toast", 117 | }); 118 | toast.present(); 119 | } 120 | 121 | async presentSuccessToast(message: string): Promise { 122 | const toast = await this.toastController.create({ 123 | message: message, 124 | duration: 500, 125 | position: "middle", 126 | color: "success", 127 | cssClass: "custom-toast", 128 | }); 129 | toast.present(); 130 | } 131 | 132 | async getSources(): Promise { 133 | const SOURCES_ENDPOINT = "/sources?"; 134 | 135 | this.newsService.getSources(SOURCES_ENDPOINT).subscribe({ 136 | next: (data: SourcesResponse) => { 137 | this.sources = data.sources; 138 | this.storageService.storeData( 139 | "storedSources", 140 | JSON.stringify(this.sources) 141 | ); 142 | this.presentSuccessToast("News sources stored successfully"); 143 | }, 144 | error: (error) => { 145 | this.presentErrorToast( 146 | `An error occurred: "${error.message}". Please try again later.` 147 | ); 148 | }, 149 | }); 150 | } 151 | 152 | loadSourceData() { 153 | this.newsService 154 | .getNews(`top-headlines?sources=${this.defaultSource}`) 155 | .subscribe({ 156 | next: (data: NewsApiResponse) => { 157 | this.sourceChosen = true; 158 | this.newsData = data.articles; 159 | }, 160 | error: (error) => { 161 | this.presentErrorToast( 162 | `An error occurred: "${error.message}". Please try again later.` 163 | ); 164 | }, 165 | }); 166 | } 167 | 168 | public trackByPublishedDate(index: number, article: Article): string { 169 | return article ? article.publishedAt : null; 170 | } 171 | 172 | public trackById(index: number, storedSources: any): string { 173 | return storedSources ? storedSources.name : null; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/app/pages/tabs/tabs.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { FormsModule } from "@angular/forms"; 4 | import { IonicModule } from "@ionic/angular"; 5 | 6 | // pages 7 | import { TabsPage } from "./tabs.page"; 8 | import { TabsPageRoutingModule } from "./tabs.router.module"; 9 | 10 | // ngx node modules 11 | import { TranslateModule } from "@ngx-translate/core"; 12 | 13 | @NgModule({ 14 | imports: [ 15 | CommonModule, 16 | FormsModule, 17 | IonicModule, 18 | TabsPageRoutingModule, 19 | TranslateModule, 20 | 21 | ], 22 | declarations: [TabsPage] 23 | }) 24 | export class TabsPageModule {} 25 | -------------------------------------------------------------------------------- /src/app/pages/tabs/tabs.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ 'TASK-BAR.news' | translate }} 7 | 8 | 9 | 10 | 11 | {{ 'TASK-BAR.categories' | translate }} 12 | 13 | 14 | 15 | 16 | {{ 'TASK-BAR.favourites' | translate }} 17 | 18 | 19 | 20 | 21 | {{ 'TASK-BAR.about' | translate }} 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/app/pages/tabs/tabs.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/app/pages/tabs/tabs.page.scss -------------------------------------------------------------------------------- /src/app/pages/tabs/tabs.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-tabs", 5 | templateUrl: "tabs.page.html", 6 | styleUrls: ["tabs.page.scss"] 7 | }) 8 | export class TabsPage {} 9 | -------------------------------------------------------------------------------- /src/app/pages/tabs/tabs.router.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { RouterModule, Routes } from "@angular/router"; 3 | import { TabsPage } from "./tabs.page"; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: "tabs", 8 | component: TabsPage, 9 | children: [ 10 | { 11 | path: "news", 12 | children: [ 13 | { 14 | path: "", 15 | loadComponent: () => 16 | import("../news/news.page").then((m) => m.NewsPage), 17 | }, 18 | ], 19 | }, 20 | { 21 | path: "news-detail", 22 | children: [ 23 | { 24 | path: "", 25 | loadComponent: () => 26 | import("../news-detail/news-detail.page").then( 27 | (m) => m.NewsDetailPage 28 | ), 29 | }, 30 | ], 31 | }, 32 | { 33 | path: "categories", 34 | children: [ 35 | { 36 | path: "", 37 | loadComponent: () => 38 | import("../categories/categories.page").then( 39 | (m) => m.CategoriesPage 40 | ), 41 | }, 42 | ], 43 | }, 44 | { 45 | path: "favourites", 46 | children: [ 47 | { 48 | path: "", 49 | loadComponent: () => 50 | import("../favourites/favourites.page").then( 51 | (m) => m.FavouritesPage 52 | ), 53 | }, 54 | ], 55 | }, 56 | { 57 | path: "about", 58 | children: [ 59 | { 60 | path: "", 61 | loadComponent: () => 62 | import("../about/about.page").then((m) => m.AboutPage), 63 | }, 64 | ], 65 | }, 66 | { 67 | path: "", 68 | redirectTo: "/tabs/news", 69 | pathMatch: "full", 70 | }, 71 | ], 72 | }, 73 | { 74 | path: "", 75 | redirectTo: "/app/tabs/news", 76 | pathMatch: "full", 77 | }, 78 | ]; 79 | 80 | @NgModule({ 81 | imports: [RouterModule.forChild(routes)], 82 | exports: [RouterModule], 83 | }) 84 | export class TabsPageRoutingModule {} 85 | -------------------------------------------------------------------------------- /src/app/pipes/date-convert.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | import * as dayjs from "dayjs"; 3 | import * as relativeTime from "dayjs/plugin/relativeTime"; 4 | 5 | dayjs.extend(relativeTime); 6 | 7 | // convert ISO8601 UTC string to '...time ago' 8 | @Pipe({ 9 | name: "dateConvert", 10 | standalone: true, 11 | }) 12 | export class DateConvertPipe implements PipeTransform { 13 | transform(value: string): string { 14 | return dayjs(value).fromNow(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | // module required so date pipe can be used by more than one page 2 | import { NgModule } from "@angular/core"; 3 | import { CommonModule } from "@angular/common"; 4 | import { IonicModule } from "@ionic/angular"; 5 | import { DateConvertPipe } from "./date-convert.pipe"; 6 | import { TitleConvertPipe } from "./title-convert.pipe"; 7 | import { TitleNosourcePipe } from "./title-nosource.pipe"; 8 | 9 | @NgModule({ 10 | imports: [CommonModule, IonicModule, DateConvertPipe, TitleConvertPipe, TitleNosourcePipe], 11 | exports: [DateConvertPipe, TitleConvertPipe, TitleNosourcePipe], 12 | }) 13 | export class PipesModule {} 14 | -------------------------------------------------------------------------------- /src/app/pipes/title-convert.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | 3 | @Pipe({ 4 | name: "titleConvert", 5 | standalone: true, 6 | }) 7 | export class TitleConvertPipe implements PipeTransform { 8 | transform(value: string): string { 9 | const shorterStrLength = 90; 10 | return value.substring(0, shorterStrLength).concat("..."); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pipes/title-nosource.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | 3 | @Pipe({ 4 | name: "titleNosource", 5 | standalone: true, 6 | }) 7 | export class TitleNosourcePipe implements PipeTransform { 8 | // 9 | transform(value: string): string { 10 | return value.replace(/-[^-]*$/, ""); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/providers/language.service.ts: -------------------------------------------------------------------------------- 1 | import { TranslateService } from "@ngx-translate/core"; 2 | import { Platform } from "@ionic/angular"; 3 | import { Injectable, OnInit } from "@angular/core"; 4 | import { Storage } from "@ionic/storage-angular"; 5 | 6 | const LNG_KEY = "SELECTED_LANGUAGE"; 7 | 8 | @Injectable({ 9 | providedIn: "root", 10 | }) 11 | export class LanguageService { 12 | selected = "en"; 13 | 14 | constructor( 15 | private translate: TranslateService, 16 | private storage: Storage, 17 | private plt: Platform 18 | ) {this.init();} 19 | 20 | async init() { 21 | const storage = await this.storage.create(); 22 | this.storage = storage; 23 | } 24 | 25 | // sets default language as browser language. Store language choice 26 | setInitialAppLanguage(): void { 27 | const language = this.translate.getBrowserLang(); 28 | this.translate.setDefaultLang(language); 29 | 30 | this.storage.get(LNG_KEY).then((val) => { 31 | if (val) { 32 | this.setLanguage(val); 33 | this.selected = val; 34 | } else { 35 | this.setLanguage("en"); 36 | this.selected = "en"; 37 | } 38 | }); 39 | } 40 | 41 | // lng can be 'en', 'fr' or 'sp' 42 | setLanguage(lng: string) { 43 | this.translate.use(lng); 44 | this.selected = lng; 45 | this.storage.set(LNG_KEY, lng); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/providers/network.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Network } from "@ionic-native/network/ngx"; 3 | import { Platform } from "@ionic/angular"; 4 | import { Observable, merge, of, fromEvent } from "rxjs"; 5 | import { mapTo } from "rxjs/operators"; 6 | 7 | @Injectable({ 8 | providedIn: "root", 9 | }) 10 | export class NetworkService { 11 | private connected: Observable = undefined; 12 | 13 | constructor(public network: Network, public platform: Platform) { 14 | this.connected = new Observable((observer) => { 15 | observer.next(true); 16 | }).pipe(mapTo(true)); 17 | 18 | if (this.platform.is("cordova")) { 19 | // on phone device 20 | this.connected = merge( 21 | this.network.onConnect().pipe(mapTo(true)), 22 | this.network.onDisconnect().pipe(mapTo(false)) 23 | ); 24 | } else { 25 | // on browser 26 | this.connected = merge( 27 | of(navigator.onLine), 28 | fromEvent(window, "online").pipe(mapTo(true)), 29 | fromEvent(window, "offline").pipe(mapTo(false)) 30 | ); 31 | } 32 | } 33 | 34 | public getNetworkType(): string { 35 | return this.network.type; 36 | } 37 | 38 | // returns network connected true or false 39 | public getNetworkStatus(): Observable { 40 | return this.connected; 41 | } 42 | 43 | public refreshPage(event: any) { 44 | setTimeout(() => { 45 | event.target.complete(); 46 | }, 2000); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/providers/news-api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnInit } from "@angular/core"; 2 | import { HttpClient } from "@angular/common/http"; 3 | import { Router } from "@angular/router"; 4 | import { throwError } from "rxjs"; 5 | import { map, catchError } from "rxjs/operators"; 6 | 7 | import { 8 | IpLocationResponse, 9 | SourcesResponse, 10 | NewsApiResponse, 11 | } from "../interfaces/interfaces"; 12 | import { Article } from "../interfaces/interfaces"; 13 | import { environment } from "../../environments/environment"; 14 | 15 | const apiUrl = environment.API_URL; 16 | const apiKey = environment.API_KEY; 17 | 18 | @Injectable({ 19 | providedIn: "root", 20 | }) 21 | export class NewsApiService implements OnInit { 22 | currentArticle: any; // used by news-detail page 23 | 24 | // fetch news from user country 25 | constructor(private http: HttpClient, private router: Router) {} 26 | 27 | ngOnInit() { 28 | this.getCountryCode(); 29 | } 30 | 31 | // fetch country code from ip location API 32 | // response.setHeader("Set-Cookie", "HttpOnly;Secure;SameSite=Strict"); 33 | getCountryCode() { 34 | return this.http.get("https://ipapi.co/json").pipe( 35 | map((data: IpLocationResponse) => data), 36 | catchError((err) => { 37 | return throwError(() => err); 38 | }) 39 | ); 40 | } 41 | 42 | // fetch sources from news API using url input 43 | getSources(url: string) { 44 | return this.http 45 | .get(`${apiUrl}/${url}&apiKey=${apiKey}`) 46 | .pipe( 47 | map((data: SourcesResponse) => data), 48 | catchError((err) => { 49 | return throwError(() => err); 50 | }) 51 | ); 52 | } 53 | 54 | // fetch news from news API using url input 55 | getNews(url: string) { 56 | return this.http 57 | .get(`${apiUrl}/${url}&apiKey=${apiKey}`) 58 | .pipe( 59 | map((data: NewsApiResponse) => data), 60 | catchError((err) => { 61 | return throwError(() => err); 62 | }) 63 | ); 64 | } 65 | 66 | // navigate to news-detail page to show article detail 67 | getNewsDetail(article: Article) { 68 | this.currentArticle = article; 69 | this.router.navigate(["/news-detail"]); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/providers/storage.service.ts: -------------------------------------------------------------------------------- 1 | import { FavouritesPage } from './../pages/favourites/favourites.page'; 2 | import { ToastService } from "./toast.service"; 3 | import { Injectable, OnInit, inject } from "@angular/core"; 4 | import { Storage } from "@ionic/storage-angular"; 5 | import { Article } from "../interfaces/interfaces"; 6 | 7 | @Injectable({ 8 | providedIn: "root", 9 | }) 10 | export class StorageService implements OnInit { 11 | private storage = inject(Storage); 12 | private toastService = inject(ToastService); 13 | // initialise a store of news articles as an empty array 14 | news: Article[] = []; 15 | 16 | async ngOnInit() { 17 | await this.storage.create(); 18 | this.storage.clear(); 19 | this.loadFavourites(); 20 | } 21 | 22 | storeData(key: string, value: string | boolean) { 23 | try { 24 | this.storage.set(key, value); 25 | // const result: string = await this.storage.get(key); 26 | // return true; 27 | } catch (err) { 28 | this.toastService.presentErrorToast( 29 | `An error occurred: "${err.message}". Please try again later.` 30 | ); 31 | } 32 | } 33 | 34 | async deleteStoredFavourites() { 35 | try { 36 | await this.storage.remove("favourites"); 37 | this.toastService.presentSuccessToast("Favourites cleared"); 38 | } catch (err) { 39 | this.toastService.presentErrorToast( 40 | `An error occurred: "${err.message}". Please try again later.` 41 | ); 42 | } 43 | } 44 | 45 | async getStoredData(key: string) { 46 | try { 47 | return this.storage.get(key); 48 | } catch (err) { 49 | this.toastService.presentErrorToast( 50 | `An error occurred: "${err.message}". Please try again later.` 51 | ); 52 | return null; 53 | } 54 | } 55 | 56 | storeCountryCode(checkedCountryCode) { 57 | this.storage.set("userCountry", checkedCountryCode); 58 | } 59 | 60 | addToFavourites(article: Article) { 61 | !this.isFavourite(article) 62 | ? this.storeArticle(article) 63 | : this.toastService.presentErrorToast( 64 | `An error occurred. Please try again later.` 65 | ); 66 | } 67 | 68 | // add new article to beginning of array so in date order. Add array to storage. 69 | storeArticle(article: Article) { 70 | this.news.unshift(article); 71 | this.storage.set("favourites", this.news); 72 | this.storeData("favourites", JSON.stringify(this.news)); 73 | this.toastService.presentSuccessToast("Article added to favourites"); 74 | } 75 | // remove article from news array and storage. 76 | removeFromFavourites(article: Article) { 77 | this.news = this.news.filter((data) => data.title !== article.title); 78 | this.storeData("favourites", JSON.stringify(this.news)); 79 | 80 | this.toastService.presentSuccessToast("Article deleted from favourites"); 81 | } 82 | 83 | // use indexOf to test if article exists in favourites array or not. 84 | isFavourite(article: Article) { 85 | return this.news.indexOf(article) !== -1; 86 | } 87 | 88 | // get array of articles from storage to list on favourites page. 89 | async loadFavourites() { 90 | const favourites = await this.storage.get("favourites"); 91 | 92 | if (favourites) { 93 | this.news = favourites; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/providers/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, RendererFactory2, Inject, Renderer2, OnInit } from "@angular/core"; 2 | 3 | import { DOCUMENT } from "@angular/common"; 4 | import { Storage } from "@ionic/storage-angular"; 5 | 6 | @Injectable({ 7 | providedIn: "root", 8 | }) 9 | 10 | // enable dark or light mode from html toggle switch event via changeThemeMode() function 11 | export class ThemeService implements OnInit{ 12 | darkMode: boolean; 13 | renderer: Renderer2; 14 | 15 | constructor ( 16 | private rendererFactory: RendererFactory2, 17 | private storage: Storage, 18 | @Inject(DOCUMENT) private document: Document 19 | ) { 20 | this.renderer = this.rendererFactory.createRenderer(null, null); 21 | } 22 | 23 | async ngOnInit() { 24 | await this.storage.create(); 25 | } 26 | 27 | enableDark() { 28 | this.renderer.addClass(this.document.body, "dark-theme"); 29 | this.storage.set("dark-theme", true); 30 | this.darkMode = true; 31 | } 32 | 33 | enableLight() { 34 | this.renderer.removeClass(this.document.body, "dark-theme"); 35 | this.storage.set("dark-theme", false); 36 | this.darkMode = false; 37 | } 38 | 39 | changeThemeMode(e: any) { 40 | e.detail.checked ? this.enableDark() : this.enableLight(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/providers/toast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, inject } from "@angular/core"; 2 | import { ToastController } from "@ionic/angular"; 3 | 4 | @Injectable({ 5 | providedIn: "root", 6 | }) 7 | export class ToastService { 8 | private toastController = inject(ToastController); 9 | 10 | async presentErrorToast(message: string): Promise { 11 | const toast = await this.toastController.create({ 12 | message: message, 13 | duration: 2000, 14 | position: "middle", 15 | color: "danger", 16 | cssClass: "custom-toast", 17 | }); 18 | toast.present(); 19 | } 20 | 21 | async presentSuccessToast(message: string): Promise { 22 | const toast = await this.toastController.create({ 23 | message: message, 24 | duration: 500, 25 | position: "middle", 26 | color: "success", 27 | cssClass: "custom-toast", 28 | }); 29 | toast.present(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "MENU": { 3 | "title": "Menu", 4 | "Nav-header": "Navigate", 5 | "Nav-settings": "Settings", 6 | "Label-language": "Language", 7 | "LangOption-English": "EN", 8 | "LangOption-Spanish": "SP", 9 | "LangOption-French": "FR", 10 | "Label-darkMode": "Dark-mode", 11 | "page-title": "" 12 | }, 13 | "NEWS": { 14 | "cancel": "Cancel" 15 | }, 16 | "NEWS-DETAIL": { 17 | "title": "News from: ", 18 | "author": "Author", 19 | "visit": "Visit website", 20 | "add-favourite": "Add to favourites", 21 | "remove-favourite": "Remove Article" 22 | }, 23 | "CATEGORIES": { 24 | "cancel": "Cancel" 25 | }, 26 | "FAVOURITES": { 27 | "photo-credit": "Photo by CapDfrawy on Unsplash", 28 | "notice": "There are no articles in Favourites" 29 | 30 | }, 31 | "PROGRESS_BAR": { 32 | "title": "Loading news..." 33 | }, 34 | "ABOUT": { 35 | "card-title": "World News", 36 | "photo-credit": "Photo by Sean O. on Unsplash", 37 | "para1-part1": "This app fetches live JSON metadata from ", 38 | "para1-part2": "a HTTP REST API with articles from about 30,000 news sources/blogs world-wide.", 39 | "para2": "Articles can be viewed in categories of general, technology, business, entertainment, health, science and sports.", 40 | "para3": "Each article includes a link to the original news source that opens in a new window." 41 | }, 42 | "TASK-BAR": { 43 | "news": "News", 44 | "categories": "Categories", 45 | "favourites": "Favourites", 46 | "about": "About" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "MENU": { 3 | "title": "Menú", 4 | "Nav-header": "Navegar", 5 | "Nav-settings": "Ajustes", 6 | "Label-language": "Idioma", 7 | "LangOption-English": "IN", 8 | "LangOption-Spanish": "ES", 9 | "LangOption-French": "FR", 10 | "Label-darkMode": "Modo Oscuro", 11 | "page-title": "" 12 | }, 13 | "NEWS": { 14 | "title": "Noticias", 15 | "author": "Autor", 16 | "source": "Fuente", 17 | "cancel": "anular" 18 | }, 19 | "NEWS-DETAIL": { 20 | "title": "Noticias de: ", 21 | "visit": "Visita pagina", 22 | "add-favourite": "Agregar favorito", 23 | "remove-favourite": "Quitar Articulo" 24 | }, 25 | "FAVOURITES": { 26 | "photo-credit": "Foto de CapDfrawy en Unsplash", 27 | "notice": "No hay articulos en favoritos" 28 | 29 | }, 30 | "PROGRESS_BAR": { 31 | "title": "Cargando noticias..." 32 | }, 33 | "ABOUT": { 34 | "page-title": "Sobre esta app", 35 | "photo-credit": "Foto de Sean O. en Unsplash", 36 | "card-title": "Noticias del Mundo", 37 | "para1-part1": "Esta aplicación obtiene metadatos JSON en vivo de ", 38 | "para1-part2": "una API HTTP REST con artículos de aproximadamente 30,000 fuentes de noticias / blogs en todo el mundo.", 39 | "para2": "Los artículos se pueden ver en categorías de general, tecnología, negocios, entretenimiento, salud, ciencia y deportes.", 40 | "para3": "Cada artículo incluye un enlace a la fuente de noticias original que se abre en una nueva ventana." 41 | }, 42 | "TASK-BAR": { 43 | "news": "Noticias", 44 | "categories": "Categorias", 45 | "favourites": "Favoritos", 46 | "about": "Sobre" 47 | } 48 | } -------------------------------------------------------------------------------- /src/assets/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "MENU": { 3 | "title": "Menu", 4 | "Nav-header": "Naviguer", 5 | "Nav-settings": "Reglage", 6 | "Label-language": "Langage", 7 | "LangOption-English": "AN", 8 | "LangOption-Spanish": "ES", 9 | "LangOption-French": "FR", 10 | "Label-darkMode": "mode-sombre" 11 | }, 12 | "NEWS": { 13 | "title": "Nouvelles", 14 | "source": "source", 15 | "cancel": "annuler" 16 | }, 17 | "NEWS-DETAIL": { 18 | "title": "Nouvelles de: ", 19 | "author": "Auteur", 20 | "visit": "Visiter site", 21 | "add-favourite": "Ajouter Favori", 22 | "remove-favourite": "Retirer Favori" 23 | }, 24 | "FAVOURITES": { 25 | "photo-credit": "Photo par CapDfrawy sur Unsplash", 26 | "notice": "Il n'y a pas d'articles dans les Favoris" 27 | }, 28 | "PROGRESS_BAR": { 29 | "title": "Chargement des nouvelles..." 30 | }, 31 | "ABOUT": { 32 | "page-title": "À Propos de Cette App", 33 | "photo-credit": "Photo par Sean O. sur Unsplash", 34 | "card-title": "Nouvelles du Monde", 35 | "para1-part1": "Cette application récupère les métadonnées JSON en direct de ", 36 | "para1-part2": "une API HTTP REST contenant des articles d'environ 30 000 sources / blogs dans le monde entier.", 37 | "para2": "Les articles peuvent être consultés dans les catégories suivantes: général, technologie, affaires, divertissement, santé, sciences et sports.", 38 | "para3": "Chaque article comprend un lien vers la source de nouvelles originale qui s'ouvre dans une nouvelle fenêtre." 39 | }, 40 | "TASK-BAR": { 41 | "news": "Nouvelles", 42 | "categories": "Catégories", 43 | "favourites": "Favoris", 44 | "about": "À Propos" 45 | } 46 | } -------------------------------------------------------------------------------- /src/assets/i18n/sp.json: -------------------------------------------------------------------------------- 1 | { 2 | "MENU": { 3 | "title": "Menú", 4 | "Nav-header": "Navegar", 5 | "Nav-settings": "Ajustes", 6 | "Label-language": "Idioma", 7 | "LangOption-English": "IN", 8 | "LangOption-Spanish": "ES", 9 | "LangOption-French": "FR", 10 | "Label-darkMode": "Modo Oscuro", 11 | "page-title": "" 12 | }, 13 | "NEWS": { 14 | "title": "Noticias", 15 | "author": "Autor", 16 | "source": "Fuente", 17 | "cancel": "anular" 18 | }, 19 | "NEWS-DETAIL": { 20 | "title": "Noticias de: ", 21 | "visit": "Visita pagina", 22 | "add-favourite": "Agregar favorito", 23 | "remove-favourite": "Quitar Articulo" 24 | }, 25 | "FAVOURITES": { 26 | "photo-credit": "Foto de CapDfrawy en Unsplash", 27 | "notice": "No hay articulos en favoritos" 28 | 29 | }, 30 | "PROGRESS_BAR": { 31 | "title": "Cargando noticias..." 32 | }, 33 | "ABOUT": { 34 | "page-title": "Sobre esta app", 35 | "photo-credit": "Foto de Sean O. en Unsplash", 36 | "card-title": "Noticias del Mundo", 37 | "para1-part1": "Esta aplicación obtiene metadatos JSON en vivo de ", 38 | "para1-part2": "una API HTTP REST con artículos de aproximadamente 30,000 fuentes de noticias / blogs en todo el mundo.", 39 | "para2": "Los artículos se pueden ver en categorías de general, tecnología, negocios, entretenimiento, salud, ciencia y deportes.", 40 | "para3": "Cada artículo incluye un enlace a la fuente de noticias original que se abre en una nueva ventana." 41 | }, 42 | "TASK-BAR": { 43 | "news": "Noticias", 44 | "categories": "Categorias", 45 | "favourites": "Favoritos", 46 | "about": "Sobre" 47 | } 48 | } -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /src/assets/imgs/about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/assets/imgs/about.jpg -------------------------------------------------------------------------------- /src/assets/imgs/en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/assets/imgs/en.png -------------------------------------------------------------------------------- /src/assets/imgs/fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/assets/imgs/fr.png -------------------------------------------------------------------------------- /src/assets/imgs/not-found.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/assets/imgs/not-found.jpg -------------------------------------------------------------------------------- /src/assets/imgs/sp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrewJBateman/ionic-angular-news-app/24f98844e666c73d8a3fc8a72050dc9b227b2358/src/assets/imgs/sp.png -------------------------------------------------------------------------------- /src/assets/pages-data.ts: -------------------------------------------------------------------------------- 1 | export const APP_PAGES = [ 2 | { 3 | id: 1, 4 | "title": { 5 | "en": "News", 6 | "fr": "Nouvelles", 7 | "sp": "Noticias" 8 | }, 9 | url: "/app/tabs/news", 10 | icon: "list", 11 | menuIcon: "menuIconNews", 12 | }, 13 | { 14 | id: 2, 15 | "title": { 16 | "en": "Categories", 17 | "fr": "Categories", 18 | "sp": "Categorias" 19 | }, 20 | url: "/app/tabs/categories", 21 | icon: "options", 22 | menuIcon: "menuIconCategories", 23 | }, 24 | { 25 | id: 3, 26 | "title": { 27 | "en": "Favourites", 28 | "fr": "Favoris", 29 | "sp": "Favoritas" 30 | }, 31 | url: "/app/tabs/favourites", 32 | icon: "heart", 33 | menuIcon: "menuIconFavourites", 34 | }, 35 | { 36 | id: 4, 37 | "title": { 38 | "en": "About", 39 | "fr": "À Propos", 40 | "sp": "Sobre esta app" 41 | }, 42 | url: "/app/tabs/about", 43 | icon: "information-circle", 44 | menuIcon: "menuIconAbout", 45 | } 46 | ] -------------------------------------------------------------------------------- /src/assets/svgs/newspaper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | API_URL: "https://newsapi.org/v2", 4 | API_KEY: "", 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 | API_URL: "https://newsapi.org/v2", 8 | API_KEY: "", 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/plugins/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | /* Core CSS required for Ionic components to work properly */ 2 | @import "~@ionic/angular/css/core.css"; 3 | 4 | /* Basic CSS for apps built with Ionic */ 5 | @import "~@ionic/angular/css/normalize.css"; 6 | @import "~@ionic/angular/css/structure.css"; 7 | @import "~@ionic/angular/css/typography.css"; 8 | @import '~@ionic/angular/css/display.css'; 9 | 10 | /* Optional CSS utils that can be commented out */ 11 | @import "~@ionic/angular/css/padding.css"; 12 | @import "~@ionic/angular/css/float-elements.css"; 13 | @import "~@ionic/angular/css/text-alignment.css"; 14 | @import "~@ionic/angular/css/text-transformation.css"; 15 | @import "~@ionic/angular/css/flex-utils.css"; 16 | 17 | @import "./app/app.scss"; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | News App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from "@angular/core"; 2 | 3 | import { environment } from "./environments/environment"; 4 | 5 | if (environment.production) { 6 | enableProdMode(); 7 | } 8 | 9 | export { renderModule } from "@angular/platform-server"; 10 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from "@angular/core"; 2 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; 3 | 4 | import { AppModule } from "./app/app.module"; 5 | import { environment } from "./environments/environment"; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | document.addEventListener("DOMContentLoaded", () => { 12 | platformBrowserDynamic() 13 | .bootstrapModule(AppModule) 14 | .catch((err) => console.log(err)); 15 | }); 16 | -------------------------------------------------------------------------------- /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"; 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 | * APPLICATION IMPORTS 55 | */ 56 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import "zone.js/testing"; 4 | import { getTestBed } from "@angular/core/testing"; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from "@angular/platform-browser-dynamic/testing"; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /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 Default (Light) Theme**/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #455A64; 8 | --ion-color-primary-rgb: 69,90,100; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255,255,255; 11 | --ion-color-primary-shade: #3d4f58; 12 | --ion-color-primary-tint: #586b74; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #0D47A1; 16 | --ion-color-secondary-rgb: 13,71,161; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255,255,255; 19 | --ion-color-secondary-shade: #0b3e8e; 20 | --ion-color-secondary-tint: #2559aa; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #F5C8F1; 24 | --ion-color-tertiary-rgb: 245,200,241; 25 | --ion-color-tertiary-contrast: #000000; 26 | --ion-color-tertiary-contrast-rgb: 0,0,0; 27 | --ion-color-tertiary-shade: #d8b0d4; 28 | --ion-color-tertiary-tint: #f6cef2; 29 | 30 | /** success **/ 31 | --ion-color-success: #008000; 32 | --ion-color-success-rgb: 0,128,0; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255,255,255; 35 | --ion-color-success-shade: #007100; 36 | --ion-color-success-tint: #1a8d1a; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffce00; 40 | --ion-color-warning-rgb: 255,206,0; 41 | --ion-color-warning-contrast: #ffffff; 42 | --ion-color-warning-contrast-rgb: 255,255,255; 43 | --ion-color-warning-shade: #e0b500; 44 | --ion-color-warning-tint: #ffd31a; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #f44336; 48 | --ion-color-danger-rgb: 244,67,54; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255,255,255; 51 | --ion-color-danger-shade: #d73b30; 52 | --ion-color-danger-tint: #f5564a; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34,34,34; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255,255,255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #989aa2; 64 | --ion-color-medium-rgb: 152,154,162; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255,255,255; 67 | --ion-color-medium-shade: #86888f; 68 | --ion-color-medium-tint: #a2a4ab; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244,244,244; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0,0,0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #f5f6f9; 77 | 78 | ion-tab-button { 79 | ion-icon, ion-label { 80 | color: #000000; 81 | } 82 | } 83 | 84 | ion-tab-button.tab-selected { 85 | ion-icon, ion-label { 86 | color: var(--ion-color-secondary) !important; 87 | --ion-color-base: var(--ion-color-secondary) !important; 88 | } 89 | 90 | ion-item.active { 91 | ion-icon, ion-label { 92 | color: var(--ion-color-secondary) !important; 93 | --ion-color-base: var(--ion-color-secondary) !important; 94 | } 95 | } 96 | 97 | } 98 | 99 | ion-toolbar { 100 | border-bottom-style: solid; 101 | border-bottom-width: 4px; 102 | border-bottom-color: var(--ion-color-secondary); 103 | } 104 | 105 | ion-tab-bar { 106 | color: var(--ion-color-light) !important; 107 | border-top-style: solid; 108 | border-top-width: 2px; 109 | border-top-color: var(--ion-color-light-shade); 110 | } 111 | 112 | ion-list-header { 113 | color: var(--ion-color-secondary); 114 | } 115 | 116 | ion-card-content a.link-text { 117 | color: var(--ion-color-secondary) !important; 118 | } 119 | 120 | .dark-theme { 121 | --ion-color-primary: #455A64; 122 | --ion-color-primary-rgb: 66,140,255; 123 | --ion-color-primary-contrast: #ffffff; 124 | --ion-color-primary-contrast-rgb: 255,255,255; 125 | --ion-color-primary-shade: #3a7be0; 126 | --ion-color-primary-tint: #5598ff; 127 | 128 | --ion-color-secondary: #50c8ff; 129 | --ion-color-secondary-rgb: 80,200,255; 130 | --ion-color-secondary-contrast: #ffffff; 131 | --ion-color-secondary-contrast-rgb: 255,255,255; 132 | --ion-color-secondary-shade: #46b0e0; 133 | --ion-color-secondary-tint: #62ceff; 134 | 135 | 136 | --ion-color-tertiary: #F5C8F1; 137 | --ion-color-tertiary-rgb: 245,200,241; 138 | --ion-color-tertiary-contrast: #000000; 139 | --ion-color-tertiary-contrast-rgb: 0,0,0; 140 | --ion-color-tertiary-shade: #d8b0d4; 141 | --ion-color-tertiary-tint: #f6cef2; 142 | 143 | --ion-color-success: #2fdf75; 144 | --ion-color-success-rgb: 47,223,117; 145 | --ion-color-success-contrast: #000000; 146 | --ion-color-success-contrast-rgb: 0,0,0; 147 | --ion-color-success-shade: #29c467; 148 | --ion-color-success-tint: #44e283; 149 | 150 | --ion-color-warning: #ffd534; 151 | --ion-color-warning-rgb: 255,213,52; 152 | --ion-color-warning-contrast: #000000; 153 | --ion-color-warning-contrast-rgb: 0,0,0; 154 | --ion-color-warning-shade: #e0bb2e; 155 | --ion-color-warning-tint: #ffd948; 156 | 157 | --ion-color-danger: #ff4961; 158 | --ion-color-danger-rgb: 255,73,97; 159 | --ion-color-danger-contrast: #ffffff; 160 | --ion-color-danger-contrast-rgb: 255,255,255; 161 | --ion-color-danger-shade: #e04055; 162 | --ion-color-danger-tint: #ff5b71; 163 | 164 | --ion-color-dark: #f4f5f8; 165 | --ion-color-dark-rgb: 244,245,248; 166 | --ion-color-dark-contrast: #000000; 167 | --ion-color-dark-contrast-rgb: 0,0,0; 168 | --ion-color-dark-shade: #d7d8da; 169 | --ion-color-dark-tint: #f5f6f9; 170 | 171 | --ion-color-medium: #989aa2; 172 | --ion-color-medium-rgb: 152,154,162; 173 | --ion-color-medium-contrast: #000000; 174 | --ion-color-medium-contrast-rgb: 0,0,0; 175 | --ion-color-medium-shade: #86888f; 176 | --ion-color-medium-tint: #a2a4ab; 177 | 178 | --ion-color-light: #222428; 179 | --ion-color-light-rgb: 34,36,40; 180 | --ion-color-light-contrast: #ffffff; 181 | --ion-color-light-contrast-rgb: 255,255,255; 182 | --ion-color-light-shade: #1e2023; 183 | --ion-color-light-tint: #383a3e; 184 | 185 | ion-content { 186 | --ion-background-color:#000; 187 | } 188 | 189 | ion-tab-button { 190 | ion-icon, ion-label { 191 | color: #fff; 192 | } 193 | } 194 | 195 | ion-list { 196 | ion-icon, ion-label { 197 | color: #fff; 198 | } 199 | 200 | ion-item { 201 | ion-icon { 202 | color: #fff; 203 | } 204 | } 205 | 206 | ion-item.active { 207 | ion-icon, ion-label { 208 | color: var(--ion-color-secondary); 209 | } 210 | } 211 | 212 | } 213 | 214 | ion-tab-button.tab-selected { 215 | ion-icon, ion-label { 216 | color: var(--ion-color-secondary) !important; 217 | --ion-color-base: var(--ion-color-secondary) !important; 218 | } 219 | 220 | ion-item.active { 221 | ion-icon, ion-label { 222 | color: var(--ion-color-secondary) !important; 223 | --ion-color-base: var(--ion-color-secondary) !important; 224 | } 225 | } 226 | 227 | } 228 | 229 | ion-col { 230 | color: var(--ion-color-primary); 231 | } 232 | 233 | ion-toolbar { 234 | border-bottom-style: solid; 235 | border-bottom-width: 4px; 236 | border-bottom-color: var(--ion-color-secondary); 237 | } 238 | 239 | ion-tab-bar { 240 | border-top-style: solid; 241 | border-top-width: 2px; 242 | border-top-color: var(--ion-color-secondary); 243 | --background: var(--ion-color-primary) !important; 244 | --color: #fff; 245 | } 246 | 247 | ion-list, ion-card, ion-searchbar { 248 | color: var(--ion-color-dark); 249 | --ion-item-background: var(--ion-color-dark-contrast); 250 | --ion-item-color: var(--ion-color-dark); 251 | 252 | ion-list-header, ion-card-title, ion-card-subtitle, span.small-text { 253 | --color: var(--ion-color-secondary); 254 | } 255 | 256 | } 257 | 258 | ion-text{ 259 | color: #fff; 260 | } 261 | 262 | ion-popover, ion-alert { 263 | background-color: var(--ion-color-dark); 264 | } 265 | } 266 | } 267 | 268 | /** Ionic CSS Variables Dark Theme**/ 269 | // .dark-theme { 270 | // --ion-color-primary: #455A64; 271 | // --ion-color-primary-rgb: 66,140,255; 272 | // --ion-color-primary-contrast: #ffffff; 273 | // --ion-color-primary-contrast-rgb: 255,255,255; 274 | // --ion-color-primary-shade: #3a7be0; 275 | // --ion-color-primary-tint: #5598ff; 276 | 277 | // --ion-color-secondary: #50c8ff; 278 | // --ion-color-secondary-rgb: 80,200,255; 279 | // --ion-color-secondary-contrast: #ffffff; 280 | // --ion-color-secondary-contrast-rgb: 255,255,255; 281 | // --ion-color-secondary-shade: #46b0e0; 282 | // --ion-color-secondary-tint: #62ceff; 283 | 284 | // --ion-color-tertiary: #6a64ff; 285 | // --ion-color-tertiary-rgb: 106,100,255; 286 | // --ion-color-tertiary-contrast: #ffffff; 287 | // --ion-color-tertiary-contrast-rgb: 255,255,255; 288 | // --ion-color-tertiary-shade: #5d58e0; 289 | // --ion-color-tertiary-tint: #7974ff; 290 | 291 | // --ion-color-success: #2fdf75; 292 | // --ion-color-success-rgb: 47,223,117; 293 | // --ion-color-success-contrast: #000000; 294 | // --ion-color-success-contrast-rgb: 0,0,0; 295 | // --ion-color-success-shade: #29c467; 296 | // --ion-color-success-tint: #44e283; 297 | 298 | // --ion-color-warning: #ffd534; 299 | // --ion-color-warning-rgb: 255,213,52; 300 | // --ion-color-warning-contrast: #000000; 301 | // --ion-color-warning-contrast-rgb: 0,0,0; 302 | // --ion-color-warning-shade: #e0bb2e; 303 | // --ion-color-warning-tint: #ffd948; 304 | 305 | // --ion-color-danger: #ff4961; 306 | // --ion-color-danger-rgb: 255,73,97; 307 | // --ion-color-danger-contrast: #ffffff; 308 | // --ion-color-danger-contrast-rgb: 255,255,255; 309 | // --ion-color-danger-shade: #e04055; 310 | // --ion-color-danger-tint: #ff5b71; 311 | 312 | // --ion-color-dark: #f4f5f8; 313 | // --ion-color-dark-rgb: 244,245,248; 314 | // --ion-color-dark-contrast: #000000; 315 | // --ion-color-dark-contrast-rgb: 0,0,0; 316 | // --ion-color-dark-shade: #d7d8da; 317 | // --ion-color-dark-tint: #f5f6f9; 318 | 319 | // --ion-color-medium: #989aa2; 320 | // --ion-color-medium-rgb: 152,154,162; 321 | // --ion-color-medium-contrast: #000000; 322 | // --ion-color-medium-contrast-rgb: 0,0,0; 323 | // --ion-color-medium-shade: #86888f; 324 | // --ion-color-medium-tint: #a2a4ab; 325 | 326 | // --ion-color-light: #222428; 327 | // --ion-color-light-rgb: 34,36,40; 328 | // --ion-color-light-contrast: #ffffff; 329 | // --ion-color-light-contrast-rgb: 255,255,255; 330 | // --ion-color-light-shade: #1e2023; 331 | // --ion-color-light-tint: #383a3e; 332 | // } 333 | 334 | /* 335 | * iOS Dark Theme 336 | * ---------------------------------------------------------------------------- 337 | */ 338 | 339 | // .dark-theme.ios { 340 | // --ion-background-color: #000000; 341 | // --ion-background-color-rgb: 0,0,0; 342 | 343 | // --ion-text-color: #ffffff; 344 | // --ion-text-color-rgb: 255,255,255; 345 | 346 | // --ion-color-step-50: #0d0d0d; 347 | // --ion-color-step-100: #1a1a1a; 348 | // --ion-color-step-150: #262626; 349 | // --ion-color-step-200: #333333; 350 | // --ion-color-step-250: #404040; 351 | // --ion-color-step-300: #4d4d4d; 352 | // --ion-color-step-350: #595959; 353 | // --ion-color-step-400: #666666; 354 | // --ion-color-step-450: #737373; 355 | // --ion-color-step-500: #808080; 356 | // --ion-color-step-550: #8c8c8c; 357 | // --ion-color-step-600: #999999; 358 | // --ion-color-step-650: #a6a6a6; 359 | // --ion-color-step-700: #b3b3b3; 360 | // --ion-color-step-750: #bfbfbf; 361 | // --ion-color-step-800: #cccccc; 362 | // --ion-color-step-850: #d9d9d9; 363 | // --ion-color-step-900: #e6e6e6; 364 | // --ion-color-step-950: #f2f2f2; 365 | 366 | // --ion-toolbar-background: #0d0d0d; 367 | 368 | // --ion-item-background: #1c1c1c; 369 | // --ion-item-background-activated: #313131; 370 | // } 371 | 372 | 373 | /* 374 | * Material Design Dark Theme 375 | * ---------------------------------------------------------------------------- 376 | */ 377 | 378 | // .dark-theme.md { 379 | // --ion-background-color: #121212; 380 | // --ion-background-color-rgb: 18,18,18; 381 | 382 | // --ion-text-color: #ffffff; 383 | // --ion-text-color-rgb: 255,255,255; 384 | 385 | // --ion-border-color: #222222; 386 | 387 | // --ion-color-step-50: #1e1e1e; 388 | // --ion-color-step-100: #2a2a2a; 389 | // --ion-color-step-150: #363636; 390 | // --ion-color-step-200: #414141; 391 | // --ion-color-step-250: #4d4d4d; 392 | // --ion-color-step-300: #595959; 393 | // --ion-color-step-350: #656565; 394 | // --ion-color-step-400: #717171; 395 | // --ion-color-step-450: #7d7d7d; 396 | // --ion-color-step-500: #898989; 397 | // --ion-color-step-550: #949494; 398 | // --ion-color-step-600: #a0a0a0; 399 | // --ion-color-step-650: #acacac; 400 | // --ion-color-step-700: #b8b8b8; 401 | // --ion-color-step-750: #c4c4c4; 402 | // --ion-color-step-800: #d0d0d0; 403 | // --ion-color-step-850: #dbdbdb; 404 | // --ion-color-step-900: #e7e7e7; 405 | // --ion-color-step-950: #f3f3f3; 406 | 407 | // --ion-item-background: #1e1e1e; 408 | 409 | // --ion-toolbar-background: #1f1f1f; 410 | 411 | // --ion-tab-bar-background: #1f1f1f; 412 | // } 413 | -------------------------------------------------------------------------------- /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 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "../models/**/*.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2022", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "skipLibCheck": true, 18 | "lib": [ 19 | "es2022", 20 | "dom" 21 | ], 22 | "allowSyntheticDefaultImports": true, 23 | "useDefineForClassFields": false 24 | }, 25 | "angularCompilerOptions": { 26 | "strictTemplates": false, 27 | "fullTemplateTypeCheck": true, 28 | "strictInjectionParameters": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | "../models/**/*.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /typings/cordova-typings.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// --------------------------------------------------------------------------------