├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── app │ ├── app.config.ts │ ├── app.html │ ├── app.routes.ts │ ├── app.scss │ ├── app.spec.ts │ ├── app.ts │ ├── components │ │ ├── donor-form │ │ │ ├── donor-form.html │ │ │ ├── donor-form.scss │ │ │ ├── donor-form.spec.ts │ │ │ └── donor-form.ts │ │ └── map │ │ │ ├── map.html │ │ │ ├── map.scss │ │ │ ├── map.spec.ts │ │ │ └── map.ts │ ├── donor.service.ts │ └── firebase-config.ts ├── index.html ├── main.ts └── styles.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Didin Jamaludin 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a Real-Time Blood Donor App with Angular 20, Google Maps, and Firebase 2 | 3 | Read full article [here](https://www.djamware.com/post/5d70d6608ff935a40f438f3a/build-a-realtime-blood-donor-app-with-angular-20-google-maps-and-firebase). 4 | 5 | This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.0. 6 | 7 | ## Development server 8 | 9 | To start a local development server, run: 10 | 11 | ```bash 12 | ng serve 13 | ``` 14 | 15 | Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. 16 | 17 | ## Code scaffolding 18 | 19 | Angular CLI includes powerful code scaffolding tools. To generate a new component, run: 20 | 21 | ```bash 22 | ng generate component component-name 23 | ``` 24 | 25 | For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: 26 | 27 | ```bash 28 | ng generate --help 29 | ``` 30 | 31 | ## Building 32 | 33 | To build the project run: 34 | 35 | ```bash 36 | ng build 37 | ``` 38 | 39 | This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. 40 | 41 | ## Running unit tests 42 | 43 | To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: 44 | 45 | ```bash 46 | ng test 47 | ``` 48 | 49 | ## Running end-to-end tests 50 | 51 | For end-to-end (e2e) testing, run: 52 | 53 | ```bash 54 | ng e2e 55 | ``` 56 | 57 | Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. 58 | 59 | ## Additional Resources 60 | 61 | For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. 62 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "blood-donor-app": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular/build:application", 19 | "options": { 20 | "browser": "src/main.ts", 21 | "polyfills": [ 22 | "zone.js" 23 | ], 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": [ 27 | { 28 | "glob": "**/*", 29 | "input": "public" 30 | } 31 | ], 32 | "styles": [ 33 | "@angular/material/prebuilt-themes/azure-blue.css", 34 | "src/styles.scss" 35 | ] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "budgets": [ 40 | { 41 | "type": "initial", 42 | "maximumWarning": "500kB", 43 | "maximumError": "1MB" 44 | }, 45 | { 46 | "type": "anyComponentStyle", 47 | "maximumWarning": "4kB", 48 | "maximumError": "8kB" 49 | } 50 | ], 51 | "outputHashing": "all" 52 | }, 53 | "development": { 54 | "optimization": false, 55 | "extractLicenses": false, 56 | "sourceMap": true 57 | } 58 | }, 59 | "defaultConfiguration": "production" 60 | }, 61 | "serve": { 62 | "builder": "@angular/build:dev-server", 63 | "configurations": { 64 | "production": { 65 | "buildTarget": "blood-donor-app:build:production" 66 | }, 67 | "development": { 68 | "buildTarget": "blood-donor-app:build:development" 69 | } 70 | }, 71 | "defaultConfiguration": "development" 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular/build:extract-i18n" 75 | }, 76 | "test": { 77 | "builder": "@angular/build:karma", 78 | "options": { 79 | "polyfills": [ 80 | "zone.js", 81 | "zone.js/testing" 82 | ], 83 | "tsConfig": "tsconfig.spec.json", 84 | "inlineStyleLanguage": "scss", 85 | "assets": [ 86 | { 87 | "glob": "**/*", 88 | "input": "public" 89 | } 90 | ], 91 | "styles": [ 92 | "@angular/material/prebuilt-themes/azure-blue.css", 93 | "src/styles.scss" 94 | ] 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blood-donor-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/cdk": "^20.0.1", 14 | "@angular/common": "^20.0.0", 15 | "@angular/compiler": "^20.0.0", 16 | "@angular/core": "^20.0.0", 17 | "@angular/fire": "^20.0.0", 18 | "@angular/forms": "^20.0.0", 19 | "@angular/google-maps": "^20.0.1", 20 | "@angular/material": "^20.0.1", 21 | "@angular/platform-browser": "^20.0.0", 22 | "@angular/router": "^20.0.0", 23 | "firebase": "^11.8.1", 24 | "rxjs": "~7.8.0", 25 | "tslib": "^2.3.0", 26 | "zone.js": "~0.15.0" 27 | }, 28 | "devDependencies": { 29 | "@angular/build": "^20.0.0", 30 | "@angular/cli": "^20.0.0", 31 | "@angular/compiler-cli": "^20.0.0", 32 | "@types/jasmine": "~5.1.0", 33 | "jasmine-core": "~5.7.0", 34 | "karma": "~6.4.0", 35 | "karma-chrome-launcher": "~3.2.0", 36 | "karma-coverage": "~2.2.0", 37 | "karma-jasmine": "~5.1.0", 38 | "karma-jasmine-html-reporter": "~2.1.0", 39 | "typescript": "~5.8.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didinj/angular-8-google-maps-firebase-realtime/9573b7551a2b153c9bdda8827483a33c7bc7f219/public/favicon.ico -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { firebaseConfig } from './firebase-config'; 6 | import { initializeApp } from 'firebase/app'; 7 | import { provideDatabase, getDatabase } from '@angular/fire/database'; 8 | import { provideAnimations } from '@angular/platform-browser/animations'; 9 | import { GoogleMapsModule } from '@angular/google-maps'; 10 | 11 | export const appConfig: ApplicationConfig = { 12 | providers: [ 13 | provideBrowserGlobalErrorListeners(), 14 | provideZoneChangeDetection({ eventCoalescing: true }), 15 | provideRouter(routes), 16 | provideFirebaseApp(() => initializeApp(firebaseConfig)), 17 | provideDatabase(() => getDatabase()), 18 | provideAnimations(), 19 | importProvidersFrom(GoogleMapsModule), 20 | ] 21 | }; 22 | function provideFirebaseApp(arg0: () => any): import("@angular/core").Provider | import("@angular/core").EnvironmentProviders { 23 | throw new Error('Function not implemented.'); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 179 | 180 |
181 |
182 |
183 | 228 |

Hello, {{ title }}

229 |

Congratulations! Your app is running. 🎉

230 |
231 | 232 |
233 |
234 | @for (item of [ 235 | { title: 'Explore the Docs', link: 'https://angular.dev' }, 236 | { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, 237 | { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, 238 | { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, 239 | { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, 240 | ]; track item.title) { 241 | 247 | {{ item.title }} 248 | 255 | 258 | 259 | 260 | } 261 |
262 | 323 |
324 |
325 |
326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = []; 4 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didinj/angular-8-google-maps-firebase-realtime/9573b7551a2b153c9bdda8827483a33c7bc7f219/src/app/app.scss -------------------------------------------------------------------------------- /src/app/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { App } from './app'; 3 | 4 | describe('App', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [App], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(App); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it('should render title', () => { 18 | const fixture = TestBed.createComponent(App); 19 | fixture.detectChanges(); 20 | const compiled = fixture.nativeElement as HTMLElement; 21 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, blood-donor-app'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/app.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | imports: [RouterOutlet], 7 | templateUrl: './app.html', 8 | styleUrl: './app.scss' 9 | }) 10 | export class App { 11 | protected title = 'blood-donor-app'; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/donor-form/donor-form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Name 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /src/app/components/donor-form/donor-form.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didinj/angular-8-google-maps-firebase-realtime/9573b7551a2b153c9bdda8827483a33c7bc7f219/src/app/components/donor-form/donor-form.scss -------------------------------------------------------------------------------- /src/app/components/donor-form/donor-form.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DonorForm } from './donor-form'; 4 | 5 | describe('DonorForm', () => { 6 | let component: DonorForm; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DonorForm] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DonorForm); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/donor-form/donor-form.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { DonorService } from '../../donor.service'; 3 | import { CommonModule } from '@angular/common'; 4 | import { MatFormFieldModule } from '@angular/material/form-field'; 5 | import { FormsModule } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'app-donor-form', 9 | imports: [CommonModule, MatFormFieldModule, FormsModule], 10 | templateUrl: './donor-form.html', 11 | styleUrl: './donor-form.scss' 12 | }) 13 | export class DonorForm { 14 | name = ''; 15 | lat = 0; 16 | lng = 0; 17 | 18 | constructor(private donorService: DonorService) { 19 | navigator.geolocation.getCurrentPosition(pos => { 20 | this.lat = pos.coords.latitude; 21 | this.lng = pos.coords.longitude; 22 | }); 23 | } 24 | 25 | submit() { 26 | this.donorService.addDonor({ name: this.name, lat: this.lat, lng: this.lng }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/map/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/map/map.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didinj/angular-8-google-maps-firebase-realtime/9573b7551a2b153c9bdda8827483a33c7bc7f219/src/app/components/map/map.scss -------------------------------------------------------------------------------- /src/app/components/map/map.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { Map } from './map'; 4 | 5 | describe('Map', () => { 6 | let component: Map; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [Map] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(Map); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/map/map.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DonorService } from '../../donor.service'; 3 | import { CommonModule } from '@angular/common'; 4 | import { GoogleMapsModule } from '@angular/google-maps'; 5 | 6 | @Component({ 7 | selector: 'app-map', 8 | imports: [CommonModule, GoogleMapsModule], 9 | templateUrl: './map.html', 10 | styleUrl: './map.scss' 11 | }) 12 | export class Map implements OnInit { 13 | donors: any[] = []; 14 | center = { lat: -6.2, lng: 106.8 }; 15 | zoom = 10; 16 | 17 | constructor(private donorService: DonorService) { } 18 | 19 | ngOnInit() { 20 | this.donorService.getDonors((data) => { 21 | this.donors = data; 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/donor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { child, getDatabase, onValue, push, ref, set } from "@angular/fire/database"; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class DonorService { 6 | db = getDatabase(); 7 | 8 | addDonor(donor: any) { 9 | const id = push(child(ref(this.db), 'donors')).key!; 10 | return set(ref(this.db, `donors/${id}`), donor); 11 | } 12 | 13 | getDonors(callback: (data: any) => void) { 14 | onValue(ref(this.db, 'donors'), (snapshot) => { 15 | const donors = snapshot.val(); 16 | callback(Object.values(donors || {})); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/firebase-config.ts: -------------------------------------------------------------------------------- 1 | // src/app/firebase-config.ts 2 | export const firebaseConfig = { 3 | apiKey: "YOUR_API_KEY", 4 | authDomain: "your-app.firebaseapp.com", 5 | databaseURL: "https://your-app.firebaseio.com", 6 | projectId: "your-app", 7 | storageBucket: "your-app.appspot.com", 8 | messagingSenderId: "XXXXXXX", 9 | appId: "YOUR_APP_ID" 10 | }; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BloodDonorApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { App } from './app/app'; 4 | 5 | bootstrapApplication(App, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "include": [ 10 | "src/**/*.ts" 11 | ], 12 | "exclude": [ 13 | "src/**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "target": "ES2022", 16 | "module": "preserve" 17 | }, 18 | "angularCompilerOptions": { 19 | "enableI18nLegacyMessageIdFormat": false, 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "typeCheckHostBindings": true, 23 | "strictTemplates": true 24 | }, 25 | "files": [], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.app.json" 29 | }, 30 | { 31 | "path": "./tsconfig.spec.json" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------