├── .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 | 
7 | 
8 | 
9 | 
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 | |  |  |  |
37 | | :----------------------------: | :----------------------------: | :---------------------------------------: |
38 | | News Page French | News Page Spanish | News Page English |
39 |
40 | |  |  |  |
41 | | :----------------------------: | :----------------------------: | :---------------------------------------: |
42 | | News Detail Page French | News Detail Page Spanish | News Detail Page English |
43 |
44 | |  |  |  |
45 | | :----------------------------------: | :----------------------------------: | :---------------------------------------------: |
46 | | Categories Page Business | Categories Page Entertainment | Article Detail Page |
47 |
48 | |  |  |  |
49 | | :----------------------------------: | :----------------------------------: | :---------------------------------------------: |
50 | | Favourites Page Empty | Favourites Page Some | Favourites Page Full |
51 |
52 | |  |  |  |
53 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: |
54 | | About Page French | About Page + Side Menu | About Page + Info Menu |
55 |
56 | |  |  |  |
57 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: |
58 | | Dark Mode News Page | Dark Mode Categories+Menu Page | Dark Mode About Page |
59 |
60 | |  |  |  |
61 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: |
62 | | Android About Dark Page | Android About Light Page | Android Categories Dark Page |
63 |
64 | |  |  |  |
65 | | :-----------------------------: | :-----------------------------: | :----------------------------------------: |
66 | | Android Detail Dark Page | Android Detail Dark Page | Android Detail Light Page |
67 |
68 | |  |  |  |
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 |
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 |

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 |
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 |
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 |
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 |
42 | }
43 |
44 | }
45 |
46 |
47 | @if (sourceChosen) {
48 |
49 | @for (article of newsData; track trackByPublishedDate) {
50 |
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 | ///
--------------------------------------------------------------------------------