├── src ├── assets │ ├── .gitkeep │ ├── logo.png │ ├── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png │ ├── fonts │ │ └── google-sans.woff2 │ └── user.svg ├── app │ ├── app.component.scss │ ├── layout │ │ ├── fez │ │ │ ├── fez.component.scss │ │ │ ├── fez.component.ts │ │ │ ├── fez.component.html │ │ │ └── fez.component.spec.ts │ │ ├── agadir │ │ │ ├── agadir.component.scss │ │ │ ├── agadir.component.ts │ │ │ ├── agadir.component.html │ │ │ └── agadir.component.spec.ts │ │ ├── chat │ │ │ ├── chat.component.scss │ │ │ ├── chat.component.spec.ts │ │ │ ├── chat.component.html │ │ │ └── chat.component.ts │ │ ├── city │ │ │ ├── city.component.scss │ │ │ ├── city.component.ts │ │ │ ├── city.component.spec.ts │ │ │ └── city.component.html │ │ ├── item │ │ │ ├── item.component.scss │ │ │ ├── item.component.spec.ts │ │ │ ├── item.component.ts │ │ │ └── item.component.html │ │ ├── login │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ ├── login.component.ts │ │ │ └── login.component.html │ │ ├── main │ │ │ ├── main.component.scss │ │ │ ├── main.component.ts │ │ │ ├── main.component.spec.ts │ │ │ └── main.component.html │ │ ├── rabat │ │ │ ├── rabat.component.scss │ │ │ ├── rabat.component.ts │ │ │ ├── rabat.component.html │ │ │ └── rabat.component.spec.ts │ │ ├── discover │ │ │ ├── discover.component.scss │ │ │ ├── discover.component.spec.ts │ │ │ ├── discover.component.html │ │ │ └── discover.component.ts │ │ ├── products │ │ │ ├── products.component.scss │ │ │ ├── products.component.spec.ts │ │ │ ├── products.component.html │ │ │ └── products.component.ts │ │ ├── sign-up │ │ │ ├── sign-up.component.scss │ │ │ ├── sign-up.component.spec.ts │ │ │ ├── sign-up.component.ts │ │ │ └── sign-up.component.html │ │ ├── tangier │ │ │ ├── tangier.component.scss │ │ │ ├── tangier.component.ts │ │ │ ├── tangier.component.html │ │ │ └── tangier.component.spec.ts │ │ ├── casablanca │ │ │ ├── casablanca.component.scss │ │ │ ├── casablanca.component.ts │ │ │ ├── casablanca.component.html │ │ │ └── casablanca.component.spec.ts │ │ ├── marrakech │ │ │ ├── marrakech.component.scss │ │ │ ├── marrakech.component.ts │ │ │ ├── marrakech.component.html │ │ │ └── marrakech.component.spec.ts │ │ └── explore │ │ │ ├── explore.component.scss │ │ │ ├── explore.component.spec.ts │ │ │ ├── explore.component.html │ │ │ └── explore.component.ts │ ├── shared │ │ ├── footer │ │ │ ├── footer.component.scss │ │ │ ├── footer.component.ts │ │ │ ├── footer.component.spec.ts │ │ │ └── footer.component.html │ │ ├── header │ │ │ ├── header.component.scss │ │ │ ├── header.component.spec.ts │ │ │ ├── header.component.ts │ │ │ └── header.component.html │ │ ├── item │ │ │ ├── item.component.scss │ │ │ ├── item.component.html │ │ │ ├── item.component.ts │ │ │ └── item.component.spec.ts │ │ ├── not-found │ │ │ ├── not-found.component.scss │ │ │ ├── not-found.component.ts │ │ │ ├── not-found.component.spec.ts │ │ │ └── not-found.component.html │ │ ├── user-avatar │ │ │ ├── user-avatar.component.scss │ │ │ ├── user-avatar.component.html │ │ │ ├── user-avatar.component.ts │ │ │ └── user-avatar.component.spec.ts │ │ └── sign-in-google │ │ │ ├── sign-in-google.component.scss │ │ │ ├── sign-in-google.component.spec.ts │ │ │ ├── sign-in-google.component.html │ │ │ └── sign-in-google.component.ts │ ├── app.component.html │ ├── components │ │ ├── card-event │ │ │ ├── card-event.component.scss │ │ │ ├── card-event.component.ts │ │ │ ├── card-event.component.html │ │ │ └── card-event.component.spec.ts │ │ ├── no-data │ │ │ ├── no-data.component.html │ │ │ └── no-data.component.ts │ │ ├── button │ │ │ ├── button.component.html │ │ │ ├── button.component.ts │ │ │ └── button.properties.ts │ │ ├── loading │ │ │ ├── loading.component.ts │ │ │ └── loading.component.html │ │ ├── input │ │ │ ├── input.properties.ts │ │ │ ├── input.component.html │ │ │ └── input.component.ts │ │ ├── tags-input │ │ │ ├── tags-input.component.html │ │ │ └── tags-input.component.ts │ │ ├── file-input │ │ │ ├── file-input.component.ts │ │ │ └── file-input.component.html │ │ └── choice │ │ │ ├── choice.component.html │ │ │ └── choice.component.ts │ ├── core │ │ ├── models │ │ │ ├── message.ts │ │ │ ├── item.ts │ │ │ └── user.ts │ │ ├── services │ │ │ ├── chat.service.spec.ts │ │ │ ├── data.service.spec.ts │ │ │ ├── authentification.service.spec.ts │ │ │ ├── chat.service.ts │ │ │ ├── data.service.ts │ │ │ └── authentification.service.ts │ │ └── data │ │ │ ├── storage.service.spec.ts │ │ │ └── storage.service.ts │ ├── app.config.server.ts │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.ts │ └── app.routes.ts ├── favicon.ico ├── main.ts ├── main.server.ts ├── styles.scss ├── environments │ ├── environment.ts │ └── environment.development.ts ├── index.html └── manifest.webmanifest ├── firestores.indexes.json ├── gitImage ├── main.jpeg ├── discover.jpeg └── architecture.png ├── database.rules.json ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .firebaserc ├── firebase.json ├── tsconfig.spec.json ├── .editorconfig ├── tsconfig.app.json ├── tailwind.config.js ├── .github └── workflows │ ├── firebase-hosting-merge.yml │ └── firebase-hosting-pull-request.yml ├── firestore.rules ├── ngsw-config.json ├── .gitignore ├── storage.rules ├── tsconfig.json ├── package.json ├── server.ts ├── README.md └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/fez/fez.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/agadir/agadir.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/chat/chat.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/city/city.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/item/item.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/login/login.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/main/main.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/rabat/rabat.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/item/item.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/layout/discover/discover.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/products/products.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/sign-up/sign-up.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/tangier/tangier.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/casablanca/casablanca.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/layout/marrakech/marrakech.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/not-found/not-found.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/user-avatar/user-avatar.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/card-event/card-event.component.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/shared/item/item.component.html: -------------------------------------------------------------------------------- 1 |

item works!

2 | -------------------------------------------------------------------------------- /src/app/shared/sign-in-google/sign-in-google.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /firestores.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /gitImage/main.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/gitImage/main.jpeg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": false, 4 | ".write": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /gitImage/discover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/gitImage/discover.jpeg -------------------------------------------------------------------------------- /gitImage/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/gitImage/architecture.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/fonts/google-sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/fonts/google-sans.woff2 -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHoussamBouzine/safeguide/HEAD/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app/core/models/message.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "@angular/fire/firestore"; 2 | export interface Message { 3 | content: string; 4 | sender: string; 5 | timestamp: Timestamp; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/no-data/no-data.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{ message }}

4 |
5 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "safeguide-fbc34": { 4 | "hosting": { 5 | "safeguide": [ 6 | "safeguide-fbc34" 7 | ] 8 | } 9 | } 10 | }, 11 | "projects": { 12 | "default": "safeguide-fbc34" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /src/app/layout/explore/explore.component.scss: -------------------------------------------------------------------------------- 1 | @keyframes bgZoom { 2 | from { 3 | background-size: 100%; 4 | } 5 | to { 6 | background-size: 110%; 7 | } 8 | } 9 | .main { 10 | animation: bgZoom 30s infinite alternate ease-in-out; 11 | } 12 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { AppComponent } from './app/app.component'; 3 | import { config } from './app/app.config.server'; 4 | 5 | const bootstrap = () => bootstrapApplication(AppComponent, config); 6 | 7 | export default bootstrap; 8 | -------------------------------------------------------------------------------- /src/app/components/button/button.component.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/app/core/models/item.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | id: string; 3 | city: string; 4 | category: string; 5 | pictures: string[]; 6 | description: string; 7 | minprice: number; 8 | maxprice: number; 9 | location: { lat: number; lon: number }; 10 | blocked: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/item/item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-item', 5 | standalone: true, 6 | imports: [], 7 | templateUrl: './item.component.html', 8 | styleUrl: './item.component.scss' 9 | }) 10 | export class ItemComponent { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/loading/loading.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-loading", 5 | standalone: true, 6 | imports: [], 7 | templateUrl: "./loading.component.html", 8 | }) 9 | export class LoadingComponent { 10 | @Input() content = "Loading ..."; 11 | } 12 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": [ 3 | { 4 | "target": "safeguide", 5 | "source": ".", 6 | "frameworksBackend": { 7 | "region": "us-central1" 8 | } 9 | } 10 | ], 11 | "storage": { 12 | "rules": "storage.rules" 13 | }, 14 | "database": { 15 | "rules": "database.rules.json" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/app/components/no-data/no-data.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-no-data", 5 | standalone: true, 6 | imports: [], 7 | templateUrl: "./no-data.component.html", 8 | }) 9 | export class NoDataComponent { 10 | @Input() icon = "ri-close-line"; 11 | @Input() message = "No data available"; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/user-avatar/user-avatar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | @if (showName) { 9 |
{{ user.displayName }}
10 | } 11 |
12 | -------------------------------------------------------------------------------- /src/assets/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [ 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/main.ts", 12 | "src/main.server.ts", 13 | "server.ts" 14 | ], 15 | "include": [ 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/app.config.server.ts: -------------------------------------------------------------------------------- 1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; 2 | import { provideServerRendering } from '@angular/platform-server'; 3 | import { appConfig } from './app.config'; 4 | 5 | const serverConfig: ApplicationConfig = { 6 | providers: [ 7 | provideServerRendering() 8 | ] 9 | }; 10 | 11 | export const config = mergeApplicationConfig(appConfig, serverConfig); 12 | -------------------------------------------------------------------------------- /src/app/core/models/user.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "@angular/fire/firestore"; 2 | 3 | export interface User { 4 | id: string; 5 | firstName: string; 6 | lastName: string; 7 | picture: string; 8 | email: string; 9 | lastLogin: Timestamp; 10 | joinedAt: Timestamp; 11 | } 12 | 13 | export interface UserSignup { 14 | firstName: string; 15 | lastName: string; 16 | email: string; 17 | password: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/layout/fez/fez.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-fez', 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './fez.component.html', 9 | styleUrl: './fez.component.scss' 10 | }) 11 | export class FezComponent { 12 | categories = ["All", "Food", "Clothing", "Transportation", "Other"]; 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/layout/rabat/rabat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-rabat', 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './rabat.component.html', 9 | styleUrl: './rabat.component.scss' 10 | }) 11 | export class RabatComponent { 12 | categories = ["All", "Food", "Clothing", "Transportation", "Other"]; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/core/services/chat.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatService } from './chat.service'; 4 | 5 | describe('ChatService', () => { 6 | let service: ChatService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ChatService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/services/data.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DataService } from './data.service'; 4 | 5 | describe('DataService', () => { 6 | let service: DataService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(DataService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | @import "remixicon/fonts/remixicon.css"; 7 | @import "ngx-toastr/toastr"; 8 | 9 | @font-face { 10 | font-family: "Google Sans"; 11 | src: url("assets/fonts/google-sans.woff2") format("woff2"); 12 | } 13 | 14 | body { 15 | @apply bg-gray-100 text-gray-700 font-sans; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/card-event/card-event.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule, NgFor } from '@angular/common'; 2 | import { Component, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-card-event', 6 | standalone: true, 7 | imports: [CommonModule], 8 | templateUrl: './card-event.component.html', 9 | styleUrl: './card-event.component.scss' 10 | }) 11 | 12 | export class CardEventComponent { 13 | @Input() event: any = {}; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/layout/agadir/agadir.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-agadir', 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './agadir.component.html', 9 | styleUrl: './agadir.component.scss' 10 | }) 11 | export class AgadirComponent { 12 | categories = ["All", "Food", "Clothing", "Transportation", "Other"]; 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/layout/tangier/tangier.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-tangier', 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './tangier.component.html', 9 | styleUrl: './tangier.component.scss' 10 | }) 11 | export class TangierComponent { 12 | categories = ["All", "Food", "Clothing", "Transportation", "Other"]; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | firebaseConfig: {"projectId":"safeguide-fbc34","appId":"1:64113152609:web:731aba4c56494825699020","databaseURL":"https://safeguide-fbc34-default-rtdb.firebaseio.com","storageBucket":"safeguide-fbc34.appspot.com","apiKey":"AIzaSyCA13_X6MhhoIT_CW6oDgp4rTM6P4O8pYc","authDomain":"safeguide-fbc34.firebaseapp.com","messagingSenderId":"64113152609","measurementId":"G-CQ9K09ZNYW"} 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /src/app/core/data/storage.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { StorageService } from './storage.service'; 4 | 5 | describe('StorageService', () => { 6 | let service: StorageService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(StorageService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/layout/marrakech/marrakech.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-marrakech', 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './marrakech.component.html', 9 | styleUrl: './marrakech.component.scss' 10 | }) 11 | export class MarrakechComponent { 12 | categories = ["All", "Food", "Clothing", "Transportation", "Other"]; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | firebaseConfig: {"projectId":"safeguide-fbc34","appId":"1:64113152609:web:731aba4c56494825699020","databaseURL":"https://safeguide-fbc34-default-rtdb.firebaseio.com","storageBucket":"safeguide-fbc34.appspot.com","apiKey":"AIzaSyCA13_X6MhhoIT_CW6oDgp4rTM6P4O8pYc","authDomain":"safeguide-fbc34.firebaseapp.com","messagingSenderId":"64113152609","measurementId":"G-CQ9K09ZNYW"} 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /src/app/layout/casablanca/casablanca.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-casablanca', 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './casablanca.component.html', 9 | styleUrl: './casablanca.component.scss' 10 | }) 11 | export class CasablancaComponent { 12 | categories = ["All", "Food", "Clothing", "Transportation", "Other"]; 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/layout/agadir/agadir.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @for(categorie of categories; track categorie){ 4 | 5 |
6 |
{{categorie}}
7 |
8 | 9 | } 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/layout/fez/fez.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @for(categorie of categories; track categorie){ 4 | 5 |
7 |
{{categorie}}
8 |
9 | 10 | } 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/layout/rabat/rabat.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @for(categorie of categories; track categorie){ 4 | 5 |
6 |
{{categorie}}
7 |
8 | 9 | } 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/layout/tangier/tangier.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @for(categorie of categories; track categorie){ 4 | 5 |
6 |
{{categorie}}
7 |
8 | 9 | } 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/layout/marrakech/marrakech.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @for(categorie of categories; track categorie){ 4 | 5 |
6 |
{{categorie}}
7 |
8 | 9 | } 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/layout/casablanca/casablanca.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @for(categorie of categories; track categorie){ 4 | 5 |
6 |
{{categorie}}
7 |
8 | 9 | } 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/shared/user-avatar/user-avatar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-user-avatar', 5 | standalone: true, 6 | imports: [], 7 | templateUrl: './user-avatar.component.html', 8 | styleUrl: './user-avatar.component.scss' 9 | }) 10 | export class UserAvatarComponent { 11 | @Input() user = { 12 | picture: "/assets/user.svg", 13 | displayName: "Unknown", 14 | }; 15 | @Input() size = 10; 16 | @Input() showName = false; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/services/authentification.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthentificationService } from './authentification.service'; 4 | 5 | describe('AuthentificationService', () => { 6 | let service: AuthentificationService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthentificationService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/shared/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | import { ButtonComponent } from '../../components/button/button.component'; 4 | 5 | @Component({ 6 | selector: 'app-not-found', 7 | standalone: true, 8 | imports: [RouterLink,ButtonComponent], 9 | templateUrl: './not-found.component.html', 10 | styleUrl: './not-found.component.scss' 11 | }) 12 | export class NotFoundComponent { 13 | goBack() { 14 | window.history.back(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import colors from "tailwindcss/colors"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ["./src/**/*.{html,ts}", "./node_modules/flowbite/**/*.js"], 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: colors.emerald, 10 | }, 11 | fontFamily: { 12 | sans: ["Google Sans", "sans-serif"], 13 | }, 14 | }, 15 | container: { 16 | center: true, 17 | padding: "2rem", 18 | }, 19 | }, 20 | plugins: [require("flowbite/plugin")], 21 | }; 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/data/storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { User } from '../models/user'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class StorageService { 8 | 9 | constructor() { } 10 | user: any; 11 | product = { 12 | title: '', 13 | desc: '', 14 | minprice: 0, 15 | maxprice: 0, 16 | picture: '', 17 | category: '', 18 | city: '' 19 | }; 20 | city = { 21 | name: '', 22 | desc: '', 23 | pictures: '', 24 | location: { 25 | lat: 0, 26 | lon: 0 27 | }, 28 | 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/layout/main/main.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | import { ChatComponent } from '../chat/chat.component'; 4 | import { HeaderComponent } from '../../shared/header/header.component'; 5 | import { FooterComponent } from '../../shared/footer/footer.component'; 6 | 7 | @Component({ 8 | selector: 'app-main', 9 | standalone: true, 10 | imports: [RouterOutlet,ChatComponent,HeaderComponent,FooterComponent], 11 | templateUrl: './main.component.html', 12 | styleUrl: './main.component.scss' 13 | }) 14 | export class MainComponent { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/layout/city/city.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { StorageService } from '../../core/data/storage.service'; 3 | import { NotFoundComponent } from '../../shared/not-found/not-found.component'; 4 | 5 | @Component({ 6 | selector: 'app-city', 7 | standalone: true, 8 | imports: [NotFoundComponent], 9 | templateUrl: './city.component.html', 10 | styleUrl: './city.component.scss' 11 | }) 12 | export class CityComponent { 13 | constructor(private storage: StorageService) { } 14 | city: any; 15 | activeImage = 0; 16 | 17 | ngOnInit() { 18 | this.city = this.storage.city; 19 | } 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from "@angular/router"; 3 | 4 | @Component({ 5 | selector: "app-footer", 6 | standalone: true, 7 | imports: [RouterLink], 8 | templateUrl: './footer.component.html', 9 | styleUrl: './footer.component.scss' 10 | }) 11 | export class FooterComponent { 12 | links = [ 13 | { label: "About", path: "/about" }, 14 | { label: "Contact", path: "/contact" }, 15 | { label: "Terms of Service", path: "/terms-of-service" }, 16 | { label: "Privacy Policy", path: "/privacy-policy" }, 17 | ]; 18 | currYear = new Date().getFullYear(); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/card-event/card-event.component.html: -------------------------------------------------------------------------------- 1 |
4 |

5 | {{ event.title }} 6 | {{ event.date }} 7 |

8 |

{{ event.content }}

9 | 10 | 11 | More info 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/layout/fez/fez.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FezComponent } from './fez.component'; 4 | 5 | describe('FezComponent', () => { 6 | let component: FezComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [FezComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(FezComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | 'on': 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: npm ci && npm run build 15 | - uses: FirebaseExtended/action-hosting-deploy@v0 16 | with: 17 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 18 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAFEGUIDE_FBC34 }}' 19 | channelId: live 20 | projectId: safeguide-fbc34 21 | -------------------------------------------------------------------------------- /src/app/layout/chat/chat.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatComponent } from './chat.component'; 4 | 5 | describe('ChatComponent', () => { 6 | let component: ChatComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ChatComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ChatComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/city/city.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CityComponent } from './city.component'; 4 | 5 | describe('CityComponent', () => { 6 | let component: CityComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CityComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CityComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/item/item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemComponent } from './item.component'; 4 | 5 | describe('ItemComponent', () => { 6 | let component: ItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ItemComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ItemComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/main/main.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MainComponent } from './main.component'; 4 | 5 | describe('MainComponent', () => { 6 | let component: MainComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [MainComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MainComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/shared/item/item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ItemComponent } from './item.component'; 4 | 5 | describe('ItemComponent', () => { 6 | let component: ItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ItemComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ItemComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [LoginComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(LoginComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/rabat/rabat.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RabatComponent } from './rabat.component'; 4 | 5 | describe('RabatComponent', () => { 6 | let component: RabatComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [RabatComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(RabatComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/agadir/agadir.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AgadirComponent } from './agadir.component'; 4 | 5 | describe('AgadirComponent', () => { 6 | let component: AgadirComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [AgadirComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(AgadirComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [FooterComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(FooterComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [HeaderComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(HeaderComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | // This rule allows anyone with your database reference to view, edit, 5 | // and delete all data in your database. It is useful for getting 6 | // started, but it is configured to expire after 30 days because it 7 | // leaves your app open to attackers. At that time, all client 8 | // requests to your database will be denied. 9 | // 10 | // Make sure to write security rules for your app before that time, or 11 | // else all client requests to your database will be denied until you 12 | // update your rules. 13 | allow read, write; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/layout/sign-up/sign-up.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SignUpComponent } from './sign-up.component'; 4 | 5 | describe('SignUpComponent', () => { 6 | let component: SignUpComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SignUpComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SignUpComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/explore/explore.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExploreComponent } from './explore.component'; 4 | 5 | describe('ExploreComponent', () => { 6 | let component: ExploreComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ExploreComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ExploreComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/tangier/tangier.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TangierComponent } from './tangier.component'; 4 | 5 | describe('TangierComponent', () => { 6 | let component: TangierComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [TangierComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(TangierComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/discover/discover.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DiscoverComponent } from './discover.component'; 4 | 5 | describe('DiscoverComponent', () => { 6 | let component: DiscoverComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DiscoverComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DiscoverComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/products/products.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductsComponent } from './products.component'; 4 | 5 | describe('ProductsComponent', () => { 6 | let component: ProductsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [ProductsComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(ProductsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/shared/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotFoundComponent } from './not-found.component'; 4 | 5 | describe('NotFoundComponent', () => { 6 | let component: NotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [NotFoundComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(NotFoundComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/marrakech/marrakech.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MarrakechComponent } from './marrakech.component'; 4 | 5 | describe('MarrakechComponent', () => { 6 | let component: MarrakechComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [MarrakechComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(MarrakechComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Safeguide 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/components/card-event/card-event.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CardEventComponent } from './card-event.component'; 4 | 5 | describe('CardEventComponent', () => { 6 | let component: CardEventComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CardEventComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CardEventComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/layout/casablanca/casablanca.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CasablancaComponent } from './casablanca.component'; 4 | 5 | describe('CasablancaComponent', () => { 6 | let component: CasablancaComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [CasablancaComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(CasablancaComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/shared/user-avatar/user-avatar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserAvatarComponent } from './user-avatar.component'; 4 | 5 | describe('UserAvatarComponent', () => { 6 | let component: UserAvatarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [UserAvatarComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(UserAvatarComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/media/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/sign-in-google/sign-in-google.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SignInGoogleComponent } from './sign-in-google.component'; 4 | 5 | describe('SignInGoogleComponent', () => { 6 | let component: SignInGoogleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SignInGoogleComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SignInGoogleComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # Firebase 45 | .firebase 46 | *-debug.log 47 | .runtimeconfig.json 48 | -------------------------------------------------------------------------------- /src/app/shared/sign-in-google/sign-in-google.component.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | 'on': pull_request 6 | permissions: 7 | checks: write 8 | contents: read 9 | pull-requests: write 10 | jobs: 11 | build_and_preview: 12 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - run: npm ci && npm run build 17 | - uses: FirebaseExtended/action-hosting-deploy@v0 18 | with: 19 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 20 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAFEGUIDE_FBC34 }}' 21 | projectId: safeguide-fbc34 22 | -------------------------------------------------------------------------------- /src/app/components/input/input.properties.ts: -------------------------------------------------------------------------------- 1 | export type InputType = 2 | | "text" 3 | | "password" 4 | | "email" 5 | | "number" 6 | | "date" 7 | | "datetime-local" 8 | | "textarea" 9 | | "select" 10 | | "hidden"; 11 | 12 | export type InputSize = "sm" | "md" | "lg"; 13 | 14 | export const inputBaseClass = [ 15 | "bg-gray-50", 16 | "border", 17 | "border-gray-300", 18 | "rounded-lg", 19 | "focus:ring-primary-600", 20 | "focus:border-primary-600", 21 | "block", 22 | "w-full", 23 | ]; 24 | 25 | export const inputAddonClass = [ 26 | "inline-flex", 27 | "px-3", 28 | "text-sm", 29 | "bg-gray-200", 30 | "border", 31 | "border-e-0", 32 | "border-gray-300", 33 | "rounded-l-md", 34 | ]; 35 | 36 | export const inputSizeClasses: Record = { 37 | sm: ["text-xs", "p-2"], 38 | md: ["text-sm", "p-2.5"], 39 | lg: ["text-md", "p-4"], 40 | }; 41 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | // Craft rules based on data in your Firestore database 4 | // allow write: if firestore.get( 5 | // /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; 6 | service firebase.storage { 7 | match /b/{bucket}/o { 8 | 9 | // This rule allows anyone with your Storage bucket reference to view, edit, 10 | // and delete all data in your Storage bucket. It is useful for getting 11 | // started, but it is configured to expire after 30 days because it 12 | // leaves your app open to attackers. At that time, all client 13 | // requests to your Storage bucket will be denied. 14 | // 15 | // Make sure to write security rules for your app before that time, or else 16 | // all client requests to your Storage bucket will be denied until you Update 17 | // your rules 18 | match /{allPaths=**} { 19 | allow read, write: if request.time < timestamp.date(2024, 3, 20); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/layout/item/item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { StorageService } from '../../core/data/storage.service'; 3 | import { NotFoundComponent } from '../../shared/not-found/not-found.component'; 4 | import { RouterLink } from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-item', 8 | standalone: true, 9 | imports: [NotFoundComponent, RouterLink], 10 | templateUrl: './item.component.html', 11 | styleUrl: './item.component.scss' 12 | }) 13 | export class ItemComponent { 14 | constructor(private storage: StorageService) { } 15 | product: any; 16 | map: any; 17 | mapCenter = { 18 | latitude: 0, 19 | longitude: 0 20 | }; 21 | ngOnInit() { 22 | this.product = this.storage.product; 23 | 24 | } 25 | locationChange(location: { latitude: number; longitude: number }) { 26 | this.product.location = { 27 | lat: location.latitude, 28 | lon: location.longitude, 29 | }; 30 | } 31 | 32 | 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'safeguide' title`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('safeguide'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello, safeguide'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/shared/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/app/shared/sign-in-google/sign-in-google.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, inject } from "@angular/core"; 2 | import { Router } from "@angular/router"; 3 | import { ToastrService } from "ngx-toastr"; 4 | import { AuthentificationService } from "../../core/services/authentification.service"; 5 | 6 | @Component({ 7 | selector: 'app-sign-in-google', 8 | standalone: true, 9 | imports: [], 10 | templateUrl: './sign-in-google.component.html', 11 | styleUrl: './sign-in-google.component.scss' 12 | }) 13 | export class SignInGoogleComponent { 14 | constructor() {} 15 | 16 | private authentification = inject(AuthentificationService); 17 | private router = inject(Router); 18 | private toastr = inject(ToastrService); 19 | @Input() disabled = false; 20 | 21 | async handleGoogleSignIn() { 22 | const result = await this.authentification.signInWithGoogle(); 23 | if (result.error || !result.user) { 24 | this.toastr.error("Oops! Something went wrong. Please try again."); 25 | return; 26 | } 27 | 28 | // Redirect to explore page 29 | this.router.navigate(["/explore"]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/layout/explore/explore.component.html: -------------------------------------------------------------------------------- 1 |
4 | 5 |
6 |

Welcome to MOROCCO

7 |

8 | Where history meets modernity, a vibrant tapestry of culture, colors, and warm hospitality. 9 |

10 | Discover 12 |
13 | 14 | 19 |
20 | -------------------------------------------------------------------------------- /src/app/components/tags-input/tags-input.component.html: -------------------------------------------------------------------------------- 1 |
2 | @if (label) { 3 | 6 | } 7 |
10 | @for (tag of tags; track tag; let index = $index) { 11 |
12 | 15 | {{ tag }} 16 | 24 | 25 |
26 | } 27 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /src/app/core/services/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | DocumentData, 4 | Firestore, 5 | QuerySnapshot, 6 | Timestamp, 7 | addDoc, 8 | collection, 9 | doc, 10 | getDoc, 11 | getDocs, 12 | limit, 13 | onSnapshot, 14 | orderBy, 15 | query, 16 | setDoc, 17 | where, 18 | } from "@angular/fire/firestore"; 19 | import { Message } from '../models/message'; 20 | import { Observable, from } from 'rxjs'; 21 | import { map } from 'rxjs/operators'; 22 | 23 | @Injectable({ 24 | providedIn: 'root' 25 | }) 26 | export class ChatService { 27 | messagesCollection = collection(this.firestore, "messages"); 28 | 29 | constructor(private firestore: Firestore) { } 30 | 31 | async getMessages(): Promise { 32 | const qry = query( 33 | this.messagesCollection, 34 | orderBy("timestamp", "asc"), 35 | ); 36 | const itemsSnapshots = await getDocs(qry); 37 | return itemsSnapshots.docs.map((doc) => doc.data() as Message); 38 | } 39 | 40 | async sendMessage(message: any): Promise { 41 | await setDoc(doc(this.firestore, "messages", message.id), message); 42 | } 43 | async createMessage(message: Partial) { 44 | const response = await addDoc(this.messagesCollection, message); 45 | return response.id; 46 | } 47 | 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/app/layout/main/main.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 | 16 | 17 |
20 |

SafeGuide Chat

21 | 26 | 27 |
28 | -------------------------------------------------------------------------------- /src/app/shared/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

6 | 14 | 19 | 20 |

21 |

Page not found

22 |

23 | The page you are looking for doesn't exist. Here are some helpful links: 24 |

25 | 26 |
27 | 33 | Go Back 34 | 35 | 36 | Take me to Explore 37 | 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /src/app/components/loading/loading.component.html: -------------------------------------------------------------------------------- 1 |
2 | 18 |

19 | {{ content }} 20 |

21 |
22 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { HeaderComponent } from "./shared/header/header.component"; 2 | import { FooterComponent } from "./shared/footer/footer.component"; 3 | import { Component, OnDestroy, OnInit, inject } from "@angular/core"; 4 | import { CommonModule } from "@angular/common"; 5 | import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; 6 | import { Auth } from "@angular/fire/auth"; 7 | import { StorageService } from "./core/data/storage.service"; 8 | import { Subscription } from "rxjs"; 9 | import { AuthentificationService } from "./core/services/authentification.service"; 10 | 11 | @Component({ 12 | selector: 'app-root', 13 | standalone: true, 14 | imports: [RouterOutlet,HeaderComponent,FooterComponent], 15 | templateUrl: './app.component.html', 16 | styleUrl: './app.component.scss' 17 | }) 18 | export class AppComponent { 19 | title = 'safeguide'; 20 | auth$!: Subscription; 21 | constructor() {} 22 | 23 | private authentication = inject(AuthentificationService); 24 | private auth = inject(Auth); 25 | private storage = inject(StorageService); 26 | private router = inject(Router); 27 | 28 | ngOnInit() { 29 | // Load user data 30 | this.auth.onAuthStateChanged((user) => { 31 | if (user) { 32 | this.auth$ = this.authentication 33 | .getUser(user.uid) 34 | .subscribe((userData) => { 35 | this.storage.user = userData; 36 | }); 37 | return; 38 | } 39 | this.storage.user = undefined; 40 | }); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/app/layout/explore/explore.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CardEventComponent } from '../../components/card-event/card-event.component'; 3 | import { RouterLink } from '@angular/router'; 4 | import { CommonModule } from '@angular/common'; 5 | 6 | @Component({ 7 | selector: 'app-explore', 8 | standalone: true, 9 | imports: [CardEventComponent,RouterLink,CommonModule], 10 | templateUrl: './explore.component.html', 11 | styleUrl: './explore.component.scss' 12 | }) 13 | export class ExploreComponent { 14 | events = [ 15 | { 16 | 'img': 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTD59g1ep4AqL2z7gHZypfediA1e1ZFkJwYhklDO5N3uQ&', 17 | 'title': 'TANJAzz - Tangier', 18 | 'date': 'June , 2024', 19 | 'content': 'Quality jazz festival in late May featuring big names from the U.S. and France. Concerts at hotels like El Minzah. Typically held in early June.' 20 | }, 21 | { 22 | 'img': 'https://www.cafonline.com/media/xpujem0o/afcon_2025.jpg', 23 | 'title': 'CAF', 24 | 'date': '2025', 25 | 'content': 'Morocco is set to host the 35th edition of the Africa Cup of Nations.' 26 | }, 27 | { 28 | 'img': 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRDYBRNit_RJtug6K3iAudlNwji_D1sGV2WzYZGF8tNug&s', 29 | 'title': 'FIFA World Cup', 30 | 'date': '2030', 31 | 'content': 'Morocco, Spain, and Portugal will jointly host the centennial 2030 FIFA World Cup, marking the 24th edition of the tournament.' 32 | } 33 | ] 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/app/layout/products/products.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Discover products

4 |
5 | 8 | 9 |
10 |
11 | 12 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/app/components/file-input/file-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-file-input", 5 | standalone: true, 6 | imports: [], 7 | templateUrl: "./file-input.component.html", 8 | }) 9 | export class FileInputComponent implements OnInit { 10 | _id: string = ""; 11 | 12 | @Input() extensions: string[] = []; 13 | @Input() multiple: boolean = false; 14 | 15 | @Output() filesSelected = new EventEmitter(); 16 | 17 | inputSelectedFiles: File[] = []; 18 | urls: string[] = []; 19 | 20 | ngOnInit(): void { 21 | this._id = Math.random().toString(36).substring(2); 22 | } 23 | 24 | onFilesSelected(event: any) { 25 | // If there is no file selected, exit 26 | if (!event.target.files) return; 27 | const files = event.target.files; 28 | 29 | // Store the selected files 30 | this.inputSelectedFiles.push(...files); 31 | 32 | // Create files preview 33 | for (let i = 0; i < files.length; i++) { 34 | let reader = new FileReader(); 35 | reader.onload = (e: any) => { 36 | this.urls.push(e.target.result); 37 | }; 38 | reader.readAsDataURL(files[i]); 39 | } 40 | 41 | // Emit the selected files 42 | this.filesSelected.emit(this.inputSelectedFiles); 43 | } 44 | 45 | removeFiles(index: number) { 46 | this.urls.splice(index, 1); 47 | this.inputSelectedFiles.splice(index, 1); 48 | 49 | // Emit the selected files 50 | this.filesSelected.emit(this.inputSelectedFiles); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safeguide", 3 | "short_name": "safeguide", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-152x152.png", 36 | "sizes": "152x152", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/icon-384x384.png", 48 | "sizes": "384x384", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/icon-512x512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/app/components/file-input/file-input.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 22 |
23 |
24 | @for (url of urls; track url; let index = $index) { 25 |
26 | 27 | 28 | 36 | 37 |
38 | } 39 |
40 |
41 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom, isDevMode } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideClientHydration } from '@angular/platform-browser'; 6 | import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; 7 | import { getAuth, provideAuth } from '@angular/fire/auth'; 8 | import { getFirestore, provideFirestore } from '@angular/fire/firestore'; 9 | import { getDatabase, provideDatabase } from '@angular/fire/database'; 10 | import { environment } from "../environments/environment.development"; 11 | import { provideToastr } from "ngx-toastr"; 12 | import { provideAnimations } from '@angular/platform-browser/animations'; 13 | import { AngularFirestoreModule } from '@angular/fire/compat/firestore'; 14 | import { provideServiceWorker } from '@angular/service-worker'; 15 | 16 | export const appConfig: ApplicationConfig = { 17 | providers: [provideRouter(routes), 18 | provideClientHydration(), 19 | importProvidersFrom(provideFirebaseApp(() => initializeApp(environment.firebaseConfig))), 20 | importProvidersFrom(provideAuth(() => getAuth())), 21 | importProvidersFrom(provideFirestore(() => getFirestore())), 22 | importProvidersFrom(provideDatabase(() => getDatabase())), 23 | provideAnimations(), 24 | provideToastr(), provideServiceWorker('ngsw-worker.js', { 25 | enabled: !isDevMode(), 26 | registrationStrategy: 'registerWhenStable:30000' 27 | }), provideServiceWorker('ngsw-worker.js', { 28 | enabled: !isDevMode(), 29 | registrationStrategy: 'registerWhenStable:30000' 30 | })] 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/components/button/button.component.ts: -------------------------------------------------------------------------------- 1 | import { NgTemplateOutlet } from "@angular/common"; 2 | import { Component, Input, OnInit } from "@angular/core"; 3 | import { 4 | ButtonColors, 5 | ButtonSizes, 6 | buttonBaseClass, 7 | buttonColorClasses, 8 | buttonSizeClasses, 9 | } from "./button.properties"; 10 | 11 | @Component({ 12 | selector: "app-button", 13 | standalone: true, 14 | imports: [NgTemplateOutlet], 15 | templateUrl: "./button.component.html", 16 | }) 17 | export class ButtonComponent implements OnInit { 18 | @Input() color: ButtonColors = "orange"; 19 | @Input() size: ButtonSizes = "md"; 20 | @Input() class: string = ""; 21 | @Input() pill: boolean = false; 22 | @Input() preIcon: string | null = null; 23 | @Input() postIcon: string | null = null; 24 | @Input() type: "button" | "submit" | "reset" = "button"; 25 | 26 | @Input() 27 | set disabled(value: boolean) { 28 | if (value) { 29 | this.buttonClasses += " opacity-50 pointer-events-none"; 30 | } else { 31 | this.buttonClasses = this.buttonClasses.replace( 32 | " opacity-50 pointer-events-none", 33 | "", 34 | ); 35 | } 36 | } 37 | 38 | buttonClasses = buttonBaseClass.join(" "); 39 | 40 | ngOnInit(): void { 41 | // Add custom classes 42 | this.buttonClasses += ` ${this.class}`; 43 | 44 | // Add color classes 45 | this.buttonClasses += ` ${buttonColorClasses[this.color].join(" ")}`; 46 | 47 | // Add size classes 48 | this.buttonClasses += ` ${buttonSizeClasses[this.size].join(" ")}`; 49 | 50 | // Add pill classes 51 | this.buttonClasses += this.pill ? " rounded-full" : " rounded-lg"; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, inject } from "@angular/core"; 2 | import { Router, RouterLink } from "@angular/router"; 3 | import { UserAvatarComponent } from "../user-avatar/user-avatar.component"; 4 | import { StorageService } from "../../core/data/storage.service"; 5 | import { initFlowbite } from "flowbite"; 6 | import { ButtonComponent } from "../../components/button/button.component"; 7 | import { AuthentificationService } from "../../core/services/authentification.service"; 8 | 9 | 10 | @Component({ 11 | selector: 'app-header', 12 | standalone: true, 13 | imports: [RouterLink, UserAvatarComponent, ButtonComponent], 14 | templateUrl: './header.component.html', 15 | styleUrl: './header.component.scss' 16 | }) 17 | 18 | export class HeaderComponent { 19 | public storage = inject(StorageService); 20 | public authentification = inject(AuthentificationService); 21 | public router = inject(Router); 22 | links = [ 23 | { label: "Discover", path: "/discover" }, 24 | { label: "Casablanca", path: "/casablanca" }, 25 | { label: "Rabat", path: "/rabat" }, 26 | { label: "Marrakech", path: "/marrakech" }, 27 | { label: "Fez", path: "/fez" }, 28 | { label: "Agadir", path: "/agadir" }, 29 | { label: "Tangier", path: "/tangier" }, 30 | ]; 31 | 32 | constructor() { } 33 | 34 | ngOnInit() { 35 | if (typeof document !== 'undefined') { 36 | initFlowbite(); 37 | } 38 | } 39 | 40 | handleSignOut() { 41 | if (confirm("Are you sure you want to sign out?")) { 42 | this.authentification.signOut(); 43 | this.router.navigate(["/login"]); 44 | this.storage.user = null; 45 | } 46 | } 47 | 48 | } 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/app/layout/products/products.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { ActivatedRoute, Route, Router, RouterLink } from '@angular/router'; 4 | import { InputComponent } from '../../components/input/input.component'; 5 | import { DataService } from '../../core/services/data.service'; 6 | import { StorageService } from '../../core/data/storage.service'; 7 | 8 | @Component({ 9 | selector: 'app-products', 10 | standalone: true, 11 | imports: [FormsModule, InputComponent, RouterLink], 12 | templateUrl: './products.component.html', 13 | styleUrl: './products.component.scss' 14 | }) 15 | export class ProductsComponent { 16 | searchQuery = ""; 17 | products: any = [] 18 | constructor(private route: ActivatedRoute, private data: DataService,private router:Router,private storage:StorageService) { } 19 | 20 | async ngOnInit() { 21 | this.route.params.subscribe(params => { 22 | const city = params['city']; 23 | const category = params['category']; 24 | if (category != "" && city != "") { 25 | this.data.getItemsByCityAndCategory(city, category).then((items) => { 26 | this.products = items; 27 | }); 28 | } else if (city != "" || category == "All") { 29 | this.data.getitemsByCity(city).then((items) => { 30 | this.products = items; 31 | }); 32 | } else { 33 | this.data.getitemsByCity(city).then((items) => { 34 | this.products = items; 35 | }) 36 | } 37 | }); 38 | 39 | } 40 | displayItem(product: any) { 41 | this.storage.product = product; 42 | this.router.navigate(['/item']); 43 | console.log(product); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/core/services/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | Firestore, 4 | Timestamp, 5 | collection, 6 | doc, 7 | getDoc, 8 | getDocs, 9 | limit, 10 | onSnapshot, 11 | orderBy, 12 | query, 13 | setDoc, 14 | where, 15 | } from "@angular/fire/firestore"; 16 | import { Item } from '../models/item'; 17 | import { Observable } from 'rxjs'; 18 | @Injectable({ 19 | providedIn: 'root' 20 | }) 21 | export class DataService { 22 | 23 | itemsCollection = collection(this.firestore, "items"); 24 | constructor(private firestore: Firestore) { } 25 | async getitemsByCity(city: string): Promise { 26 | const qry = query( 27 | this.itemsCollection, 28 | where("city", "==", city), 29 | limit(100), 30 | ); 31 | const itemsSnapshots = await getDocs(qry); 32 | return itemsSnapshots.docs.map((doc) => doc.data() as Item); 33 | } 34 | async getitemsByCategory(category: string): Promise { 35 | const qry = query( 36 | this.itemsCollection, 37 | where("category", "==", category), 38 | limit(100), 39 | ); 40 | const itemsSnapshots = await getDocs(qry); 41 | return itemsSnapshots.docs.map((doc) => doc.data() as Item); 42 | } 43 | async getItemsByCityAndCategory(city: string, category: string): Promise { 44 | if (category != "" && city != "") { 45 | const qry = query( 46 | this.itemsCollection, 47 | where("city", "==", city), 48 | where("category", "==", category), 49 | limit(100), 50 | ); 51 | const itemsSnapshots = await getDocs(qry); 52 | return itemsSnapshots.docs.map((doc) => doc.data() as Item); 53 | } else { 54 | return []; 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/app/layout/chat/chat.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Public Chat 5 | 6 | 7 | Online 8 | 9 |
10 |
11 |
12 |

Feel free to share information or answer other users.

13 |
14 |
15 |
16 | @for(msg of messages; track msg){ 17 |
18 | user_image 19 |
20 |

21 | {{ msg.sender}} {{ time(msg.timestamp.toDate()) }} 22 |

23 |
24 |

{{msg.content}}

25 |
26 |
27 |
28 | } 29 |
30 | 31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safeguide", 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 | "serve:ssr:safeguide": "node dist/safeguide/server/server.mjs" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^17.1.0", 15 | "@angular/common": "^17.1.0", 16 | "@angular/compiler": "^17.1.0", 17 | "@angular/core": "^17.1.0", 18 | "@angular/fire": "^17.0.1", 19 | "@angular/forms": "^17.1.0", 20 | "@angular/platform-browser": "^17.1.0", 21 | "@angular/platform-browser-dynamic": "^17.1.0", 22 | "@angular/platform-server": "^17.1.0", 23 | "@angular/router": "^17.1.0", 24 | "@angular/service-worker": "^17.2.3", 25 | "@angular/ssr": "^17.1.0", 26 | "dayjs": "^1.11.10", 27 | "express": "^4.18.2", 28 | "firebase": "^10.8.0", 29 | "flowbite": "^2.2.1", 30 | "ngx-leaflet": "^0.0.16", 31 | "ngx-toastr": "^18.0.0", 32 | "remixicon": "^4.2.0", 33 | "rxjs": "~7.8.0", 34 | "tslib": "^2.3.0", 35 | "zone.js": "~0.14.3" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "^17.1.0", 39 | "@angular/cli": "^17.1.0", 40 | "@angular/compiler-cli": "^17.1.0", 41 | "@types/express": "^4.17.17", 42 | "@types/jasmine": "~5.1.0", 43 | "@types/node": "^18.18.0", 44 | "autoprefixer": "^10.4.17", 45 | "jasmine-core": "~5.1.0", 46 | "karma": "~6.4.0", 47 | "karma-chrome-launcher": "~3.2.0", 48 | "karma-coverage": "~2.2.0", 49 | "karma-jasmine": "~5.1.0", 50 | "karma-jasmine-html-reporter": "~2.1.0", 51 | "postcss": "^8.4.35", 52 | "tailwindcss": "^3.4.1", 53 | "typescript": "~5.3.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/layout/discover/discover.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Discover Moroccan Cities

4 |
5 | 8 | 9 |
10 |
11 |
12 | @for(city of cities; track city){ 13 | 33 | 34 | } 35 |
36 | 37 |
38 | -------------------------------------------------------------------------------- /src/app/components/choice/choice.component.html: -------------------------------------------------------------------------------- 1 |
2 | @if (label) { 3 |
Sort By
4 | } 5 |
6 | @for (choice of choices; track $index) { 7 | @if (isToggle) { 8 | 28 | } @else { 29 |
30 | 40 | 43 |
44 | } 45 | } 46 |
47 |
48 | -------------------------------------------------------------------------------- /src/app/layout/chat/chat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { InputComponent } from '../../components/input/input.component'; 3 | import { ButtonComponent } from '../../components/button/button.component'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { Timestamp } from 'firebase/firestore'; 6 | import { StorageService } from '../../core/data/storage.service'; 7 | import { Message } from '../../core/models/message'; 8 | import { ChatService } from '../../core/services/chat.service'; 9 | import { Subscription, Observable } from 'rxjs'; 10 | import { map } from 'rxjs/operators'; 11 | 12 | import dayjs from "dayjs"; 13 | @Component({ 14 | selector: 'app-chat', 15 | standalone: true, 16 | imports: [InputComponent, ButtonComponent, FormsModule], 17 | templateUrl: './chat.component.html', 18 | styleUrl: './chat.component.scss' 19 | }) 20 | export class ChatComponent { 21 | messageChat = ''; 22 | private chatService: ChatService = inject(ChatService); 23 | private storage: StorageService = inject(StorageService); 24 | messages: any[] = []; 25 | subscription: Subscription | undefined; 26 | 27 | async ngOnInit() { 28 | await this.chatService.getMessages().then((items) => { 29 | this.messages = items; 30 | }); 31 | } 32 | 33 | send() { 34 | let sender: string; 35 | if (this.storage.user && this.storage.user.lastName) { 36 | sender = this.storage.user.lastName; 37 | } else { 38 | sender = 'user'; 39 | } 40 | var message = { 41 | content: this.messageChat, 42 | sender: sender, 43 | timestamp: Timestamp.now() 44 | }; 45 | this.chatService.createMessage(message); 46 | this.messages.push(message); 47 | this.messageChat = ''; 48 | } 49 | 50 | time(date: Date): string { 51 | return dayjs(date).format('dddd [at] HH:mm A'); 52 | } 53 | 54 | 55 | 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import { APP_BASE_HREF } from '@angular/common'; 2 | import { CommonEngine } from '@angular/ssr'; 3 | import express from 'express'; 4 | import { fileURLToPath } from 'node:url'; 5 | import { dirname, join, resolve } from 'node:path'; 6 | import bootstrap from './src/main.server'; 7 | 8 | // The Express app is exported so that it can be used by serverless Functions. 9 | export function app(): express.Express { 10 | const server = express(); 11 | const serverDistFolder = dirname(fileURLToPath(import.meta.url)); 12 | const browserDistFolder = resolve(serverDistFolder, '../browser'); 13 | const indexHtml = join(serverDistFolder, 'index.server.html'); 14 | 15 | const commonEngine = new CommonEngine(); 16 | 17 | server.set('view engine', 'html'); 18 | server.set('views', browserDistFolder); 19 | 20 | // Example Express Rest API endpoints 21 | // server.get('/api/**', (req, res) => { }); 22 | // Serve static files from /browser 23 | server.get('*.*', express.static(browserDistFolder, { 24 | maxAge: '1y' 25 | })); 26 | 27 | // All regular routes use the Angular engine 28 | server.get('*', (req, res, next) => { 29 | const { protocol, originalUrl, baseUrl, headers } = req; 30 | 31 | commonEngine 32 | .render({ 33 | bootstrap, 34 | documentFilePath: indexHtml, 35 | url: `${protocol}://${headers.host}${originalUrl}`, 36 | publicPath: browserDistFolder, 37 | providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], 38 | }) 39 | .then((html) => res.send(html)) 40 | .catch((err) => next(err)); 41 | }); 42 | 43 | return server; 44 | } 45 | 46 | function run(): void { 47 | const port = process.env['PORT'] || 4000; 48 | 49 | // Start up the Node server 50 | const server = app(); 51 | server.listen(port, () => { 52 | console.log(`Node Express server listening on http://localhost:${port}`); 53 | }); 54 | } 55 | 56 | run(); 57 | -------------------------------------------------------------------------------- /src/app/components/tags-input/tags-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from "@angular/core"; 2 | import { 3 | ControlValueAccessor, 4 | FormsModule, 5 | NG_VALUE_ACCESSOR, 6 | } from "@angular/forms"; 7 | 8 | @Component({ 9 | selector: "app-tags-input", 10 | standalone: true, 11 | imports: [FormsModule], 12 | providers: [ 13 | { 14 | provide: NG_VALUE_ACCESSOR, 15 | useExisting: TagsInputComponent, 16 | multi: true, 17 | }, 18 | ], 19 | 20 | templateUrl: "./tags-input.component.html", 21 | }) 22 | export class TagsInputComponent implements ControlValueAccessor { 23 | @Output() tagsChange = new EventEmitter(); 24 | 25 | @Input() label: string | null = null; 26 | @Input() placeholder: string = ""; 27 | 28 | _id: string = Math.random().toString(36).substring(2); 29 | tags: string[] = []; 30 | inputTagValue: string = ""; 31 | 32 | touched = false; 33 | onChange = (value: any) => {}; 34 | onTouched = () => {}; 35 | 36 | addTag() { 37 | this.markAsTouched(); 38 | const tag = this.inputTagValue.trim(); 39 | 40 | if (tag && !this.tags.includes(tag)) { 41 | this.tags.push(tag); 42 | this.inputTagValue = ""; 43 | this.onChange(this.tags); 44 | } 45 | } 46 | 47 | removeTag(index: number) { 48 | this.tags.splice(index, 1); 49 | this.onChange(this.tags); 50 | } 51 | 52 | removeLastTag() { 53 | if (this.inputTagValue.length > 0) { 54 | return; 55 | } 56 | 57 | this.tags.pop(); 58 | this.onChange(this.tags); 59 | } 60 | 61 | markAsTouched(): void { 62 | if (this.touched) return; 63 | 64 | this.onTouched(); 65 | this.touched = true; 66 | } 67 | 68 | writeValue(tags: string[]): void { 69 | this.tags = tags; 70 | } 71 | 72 | registerOnChange(fn: any): void { 73 | this.onChange = fn; 74 | } 75 | 76 | registerOnTouched(fn: any): void { 77 | this.onTouched = fn; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/layout/sign-up/sign-up.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from "@angular/core"; 2 | import { FormsModule } from "@angular/forms"; 3 | import { Router, RouterLink } from "@angular/router"; 4 | import { ToastrService } from "ngx-toastr"; 5 | import { HeaderComponent } from "../../shared/header/header.component"; 6 | import { ButtonComponent } from "../../components/button/button.component"; 7 | import { InputComponent } from "../../components/input/input.component"; 8 | import { SignInGoogleComponent } from "../../shared/sign-in-google/sign-in-google.component"; 9 | import { AuthentificationService } from "../../core/services/authentification.service"; 10 | 11 | @Component({ 12 | selector: "app-sign-up", 13 | standalone: true, 14 | imports: [ 15 | RouterLink, 16 | FormsModule, 17 | SignInGoogleComponent, 18 | HeaderComponent, 19 | ButtonComponent, 20 | InputComponent, 21 | ], 22 | templateUrl: './sign-up.component.html', 23 | styleUrl: './sign-up.component.scss' 24 | }) 25 | export class SignUpComponent { 26 | constructor() { } 27 | 28 | private authentification = inject(AuthentificationService); 29 | private router = inject(Router); 30 | private toastr = inject(ToastrService); 31 | 32 | showPassword = false; 33 | buttonsDisabled = false; 34 | 35 | user = { 36 | firstName: "", 37 | lastName: "", 38 | email: "", 39 | password: "", 40 | }; 41 | 42 | async handleSubmit() { 43 | this.buttonsDisabled = true; 44 | console.log(this.user); 45 | // TODO: Add form validation 46 | 47 | // Sign up 48 | const result = await this.authentification.signUp(this.user); 49 | if (result.error || !result.user) { 50 | switch (result.error.code) { 51 | case "auth/email-already-in-use": 52 | this.toastr.error("Oops! This email is already in use"); 53 | break; 54 | default: 55 | this.toastr.error("Oops! Something went wrong"); 56 | break; 57 | } 58 | console.log(result.user); 59 | this.buttonsDisabled = false; 60 | return; 61 | } 62 | 63 | // Redirect to explore page 64 | this.router.navigate(["/login"]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/shared/header/header.component.html: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /src/app/layout/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from "@angular/core"; 2 | import { FormsModule } from "@angular/forms"; 3 | import { Router, RouterLink } from "@angular/router"; 4 | import { ToastrService } from "ngx-toastr"; 5 | import { HeaderComponent } from "../../shared/header/header.component"; 6 | import { ButtonComponent } from "../../components/button/button.component"; 7 | import { InputComponent } from "../../components/input/input.component"; 8 | import { AuthentificationService } from "../../core/services/authentification.service"; 9 | import { SignInGoogleComponent } from "../../shared/sign-in-google/sign-in-google.component"; 10 | import { StorageService } from "../../core/data/storage.service"; 11 | 12 | @Component({ 13 | selector: "app-login", 14 | standalone: true, 15 | imports: [ 16 | RouterLink, 17 | FormsModule, 18 | SignInGoogleComponent, 19 | HeaderComponent, 20 | ButtonComponent, 21 | InputComponent, 22 | ], 23 | templateUrl: "./login.component.html", 24 | }) 25 | export class LoginComponent { 26 | private authentication = inject(AuthentificationService); 27 | private router = inject(Router); 28 | private toastr = inject(ToastrService); 29 | private storage = inject(StorageService); 30 | 31 | constructor() {} 32 | 33 | showPassword = false; 34 | buttonsDisabled = false; 35 | user = { 36 | email: "", 37 | password: "", 38 | }; 39 | 40 | async handleSubmit() { 41 | this.buttonsDisabled = true; 42 | 43 | // TODO: Add form validation 44 | 45 | // Sign in user 46 | const result = await this.authentication.signIn( 47 | this.user.email, 48 | this.user.password, 49 | ); 50 | if (result.error) { 51 | switch (result.error.code) { 52 | case "auth/user-not-found" || "auth/wrong-password": 53 | this.toastr.error("Oops! The email or password is incorrect"); 54 | break; 55 | default: 56 | this.toastr.error("Oops! Something went wrong"); 57 | break; 58 | } 59 | this.buttonsDisabled = false; 60 | return; 61 | } 62 | // Redirect to explore page 63 | this.router.navigate(["/discover"]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/components/input/input.component.html: -------------------------------------------------------------------------------- 1 |
2 | @if (label) { 3 | 6 | } 7 |
8 | @if (addon) { 9 | 10 | 11 | 12 | } @else { 13 | 14 | } 15 |
16 | @if (preIcon) { 17 |
21 | 22 |
23 | } 24 | @if (input === "textarea") { 25 | 34 | } @else if (input === "select") { 35 | 55 | } @else { 56 | 66 | } 67 | @if (postIcon) { 68 |
73 | 74 |
75 | } 76 |
77 |
78 |
79 | -------------------------------------------------------------------------------- /src/app/components/button/button.properties.ts: -------------------------------------------------------------------------------- 1 | export type ButtonColors = 2 | | "primary" 3 | | "dark" 4 | | "light" 5 | | "info" 6 | | "success" 7 | | "warning" 8 | | "danger" 9 | | "indigo" 10 | | "orange"; 11 | 12 | export type ButtonSizes = "xs" | "sm" | "md" | "lg" | "xl"; 13 | 14 | export const buttonBaseClass = [ 15 | "group", 16 | "flex", 17 | "items-center", 18 | "justify-center", 19 | "text-center", 20 | "font-medium", 21 | "focus:z-10", 22 | "focus:outline-none", 23 | ]; 24 | 25 | export const buttonColorClasses: Record = { 26 | primary: [ 27 | "text-white", 28 | "bg-primary-600", 29 | "hover:bg-primary-700", 30 | "focus:ring-4", 31 | "focus:ring-primary-100", 32 | ], 33 | dark: [ 34 | "text-white", 35 | "bg-gray-800", 36 | "hover:bg-gray-900", 37 | "focus:ring-4", 38 | "focus:ring-gray-300", 39 | ], 40 | light: [ 41 | "text-gray-900", 42 | "bg-white", 43 | "border", 44 | "border-gray-300", 45 | "hover:bg-gray-100", 46 | "focus:ring-4", 47 | "focus:ring-blue-300", 48 | ], 49 | info: [ 50 | "text-white", 51 | "bg-blue-700", 52 | "hover:bg-blue-800", 53 | "focus:ring-4", 54 | "focus:ring-blue-300", 55 | ], 56 | success: [ 57 | "text-white", 58 | "bg-green-700", 59 | "hover:bg-green-800", 60 | "focus:ring-4", 61 | "focus:ring-green-100", 62 | ], 63 | warning: [ 64 | "text-white", 65 | "bg-yellow-400", 66 | "hover:bg-yellow-500", 67 | "focus:ring-4", 68 | "focus:ring-yellow-200", 69 | ], 70 | danger: [ 71 | "text-white", 72 | "bg-red-700", 73 | "hover:bg-red-800", 74 | "focus:ring-4", 75 | "focus:ring-red-300", 76 | ], 77 | indigo: [ 78 | "text-white", 79 | "bg-indigo-600", 80 | "hover:bg-indigo-700", 81 | "focus:ring-4", 82 | "focus:ring-indigo-300", 83 | ], 84 | orange: [ 85 | "text-white", 86 | "bg-orange-700", 87 | "hover:bg-orange-800", 88 | "focus:ring-4", 89 | "focus:ring-indigo-300", 90 | ], 91 | }; 92 | 93 | export const buttonSizeClasses: Record = { 94 | xs: ["text-xs", "py-2", "px-3"], 95 | sm: ["text-sm", "py-2", "px-3"], 96 | md: ["text-sm", "px-5", "py-2.5"], 97 | lg: ["text-base", "py-3", "px-5"], 98 | xl: ["text-base", "px-6", "py-3.5"], 99 | }; 100 | -------------------------------------------------------------------------------- /src/app/layout/city/city.component.html: -------------------------------------------------------------------------------- 1 | @if (city) { 2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 | @for (picture of city.images; track $index) { 10 | 15 | } 16 |
17 |
18 |
19 |
20 |
21 |
{{ city.name }}
22 |
23 |
24 |
25 | 26 |

0.3km

27 |
28 |
29 | 30 |

2 hours

31 |
32 |
33 |

{{ city.desc }}

34 |
35 |
36 |
37 |
Stadium
38 | 39 |

{{ city.stadiumDesc }}

40 |
41 | 42 |
43 |
44 |
Stadium Location:
45 |
46 | 49 |
50 | 51 |
52 | } @else { 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/app/layout/sign-up/sign-up.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Create new account

4 | 5 |
6 |
7 | or 8 |
9 |
10 |
11 |
12 | 22 | 32 |
33 | 44 | 56 | 62 | @if (buttonsDisabled) { 63 |
64 | 65 |
66 | } @else { 67 | Create account 68 | } 69 |
70 |

71 | Already registered? 72 | 76 | Login 77 | 78 |

79 | 80 |
81 |
82 | -------------------------------------------------------------------------------- /src/app/layout/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sign in to your account

4 | 5 |
6 |
7 | or 8 |
9 |
10 |
11 | 22 | 34 |
35 |
36 |
37 | 43 |
44 |
45 | 46 |
47 |
48 | 49 | Forgot password? 50 | 51 |
52 | 58 | @if (buttonsDisabled) { 59 |
60 | 61 |
62 | } @else { 63 | Sign in 64 | } 65 |
66 |

67 | Not registered? 68 | 72 | Create an account 73 | 74 |

75 | 76 |
77 |
78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌟 SafeGuide 2 | 3 | ## ✨ Discover Morocco - Authentically, Safely, Affordably 4 | 5 | Welcome to SafeGuide - the quintessential digital compass for the savvy traveler venturing into the heart of Morocco. With our Progressive Web App (PWA), powered by the robust trio of Angular, Tailwind CSS, and Firebase, you're all set for a journey that's as seamless as it is secure. 6 | 7 | ## Features 8 | 9 | - **Progressive Web App (PWA)**: Enjoy the convenience of accessing the app directly from your mobile device's browser without the need to download or install from an app store. 10 | - **Real-Time Price Display**: View real-time prices of various Moroccan products to ensure transparency and prevent scams. 11 | - **User-Friendly Interface**: Enjoy a user-friendly interface designed for tourists to easily access and understand price information. 12 | - **Geolocation Integration**: Utilize geolocation features to suggest nearby markets or shops where users can find the displayed products. 13 | - **Chat Section**: Engage in conversations with other users to share experiences, ask questions, and exchange travel tips and recommendations. 14 | - **User Interaction**: Interact with fellow tourists through a chat interface to enhance the overall travel experience and foster a sense of community. 15 | -**Product Categories**: Organize products into categories for easy navigation and browsing. 16 | 17 | 18 | ## Tech Stack 19 | 20 | - **Frontend**: Crafted with Angular Framework for finesse and fines. 21 | - **Styling**: Dressed in Tailwind CSS and Flowbite for a responsive, tailor-made UI. 22 | - **Backend & Security**: Fortified by Firebase's full suite for peace of mind. 23 | 24 | ## Architecture 25 | 26 |

27 | Architecture 28 |

29 | 30 | We encountered hosting issues with Firebase, so we opted to host our application on Netlify instead. 31 | 32 | 33 | ## Explore SafeGuide Online 34 | 35 | You can experience SafeGuide online by visiting [https://safeguide.netlify.app/explore](https://safeguide.netlify.app/explore). 36 | 37 | --- 38 | 39 | ## Getting Started 40 | 41 | To run SafeGuide locally, follow these steps:: 42 | 43 | 1. 📥 Clone the repository : `https://github.com/AhmedHoussamBouzine/safeguide.git`. 44 | 2. 🌐 Install Angular CLI with `npm install -g @angular/cli`. 45 | 3. 📦 Install the dependencies with `npm install`. 46 | 4. 🚀 Launch with `ng serve`. 47 | 5. 🌍 Visit `http://localhost:4200/` to explore. 48 | 49 | ## Contributing 50 | 51 | Contributions to *SafeGuide* are welcome! If you have ideas for new features, improvements, or bug fixes, please open an issue or submit a pull request. 52 | 53 | --- 54 | 55 | 👨‍💻 Crafted by N7 Team - ENSET Mohammedia, 2024. 56 | 57 | -------------------------------------------------------------------------------- /src/app/components/choice/choice.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core"; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; 3 | 4 | @Component({ 5 | selector: "app-choice", 6 | standalone: true, 7 | imports: [], 8 | templateUrl: "./choice.component.html", 9 | providers: [ 10 | { 11 | provide: NG_VALUE_ACCESSOR, 12 | useExisting: ChoiceComponent, 13 | multi: true, 14 | }, 15 | ], 16 | }) 17 | export class ChoiceComponent implements OnInit, ControlValueAccessor { 18 | @Input() label: string | null = null; 19 | @Input() choices: { label: string; value: string; id?: string }[] = []; 20 | @Input() name: string = Math.random().toString(36).substring(2); 21 | @Input() multiple: boolean = false; 22 | @Input() isToggle: boolean = false; 23 | 24 | inputClass: string = ""; 25 | 26 | value: any; 27 | touched = false; 28 | onChange = (value: any) => {}; 29 | onTouched = () => {}; 30 | 31 | ngOnInit(): void { 32 | // Generate a unique id for each choice 33 | this.choices = this.choices.map((choice) => { 34 | choice.id = choice.id || Math.random().toString(36).substring(2); 35 | return choice; 36 | }); 37 | 38 | this.inputClass += this.multiple ? " rounded" : " rounded-full"; 39 | } 40 | 41 | onChoice($event: Event) { 42 | this.markAsTouched(); 43 | 44 | const target = $event.target as HTMLInputElement; 45 | const value = target.value; 46 | 47 | // If it's a radio input, we set the value directly 48 | if (!this.multiple) { 49 | this.value = value; 50 | this.onChange(this.value); 51 | return; 52 | } 53 | 54 | // Otherwise 55 | // If we have a single choice, we toggle the value (true | false) 56 | if (this.choices.length === 1) { 57 | this.value = !this.value; 58 | } else { 59 | // If we have multiple choices, we toggle the value in the array 60 | this.value = this.value.includes(value) 61 | ? this.value.filter((v: string) => v !== value) 62 | : [...this.value, value]; 63 | } 64 | 65 | this.onChange(this.value); 66 | } 67 | 68 | isChecked(value: any): boolean { 69 | if (this.multiple) { 70 | return this.choices.length === 1 71 | ? this.value // If we have a single choice, we check if the value is true 72 | : this.value && this.value.includes(value); // If we have multiple choices, we check if the value is in the array 73 | } 74 | 75 | // Otherwise, we check if the value is the same as the input value 76 | return this.value === value; 77 | } 78 | 79 | markAsTouched(): void { 80 | if (this.touched) return; 81 | 82 | this.onTouched(); 83 | this.touched = true; 84 | } 85 | 86 | writeValue(obj: any): void { 87 | this.value = obj; 88 | } 89 | 90 | registerOnChange(fn: any): void { 91 | this.onChange = fn; 92 | } 93 | 94 | registerOnTouched(fn: any): void { 95 | this.onTouched = fn; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { MainComponent } from './layout/main/main.component'; 3 | import { ExploreComponent } from './layout/explore/explore.component'; 4 | import { SignUpComponent } from './layout/sign-up/sign-up.component'; 5 | import { LoginComponent } from './layout/login/login.component'; 6 | import { NotFoundComponent } from './shared/not-found/not-found.component'; 7 | import { FezComponent } from './layout/fez/fez.component'; 8 | import { CasablancaComponent } from './layout/casablanca/casablanca.component'; 9 | import { RabatComponent } from './layout/rabat/rabat.component'; 10 | import { AgadirComponent } from './layout/agadir/agadir.component'; 11 | import { TangierComponent } from './layout/tangier/tangier.component'; 12 | import { MarrakechComponent } from './layout/marrakech/marrakech.component'; 13 | import { DiscoverComponent } from './layout/discover/discover.component'; 14 | import { ProductsComponent } from './layout/products/products.component'; 15 | import { ChatComponent } from './layout/chat/chat.component'; 16 | import { ItemComponent } from './layout/item/item.component'; 17 | import { 18 | canActivate, 19 | redirectLoggedInTo, 20 | redirectUnauthorizedTo, 21 | } from "@angular/fire/auth-guard"; 22 | import { CityComponent } from './layout/city/city.component'; 23 | 24 | export const routes: Routes = [ 25 | { 26 | path: "", 27 | redirectTo: "/explore", 28 | pathMatch: "full", 29 | }, 30 | { 31 | path: "", 32 | component: MainComponent, 33 | children: [ 34 | { 35 | path: "fez", 36 | component: FezComponent, 37 | }, 38 | { 39 | path: ":city/:category/products", 40 | component: ProductsComponent, 41 | }, 42 | { 43 | path: ":city/products", 44 | component: ProductsComponent, 45 | }, 46 | { 47 | path: ":city/infos", 48 | component: CityComponent, 49 | }, 50 | { 51 | path: "discover", 52 | component: DiscoverComponent, 53 | }, 54 | { 55 | path: "chat", 56 | component: ChatComponent, 57 | }, 58 | { 59 | path: "item", 60 | component: ItemComponent, 61 | }, 62 | { 63 | path: "casablanca", 64 | component: CasablancaComponent, 65 | }, 66 | { 67 | path: "rabat", 68 | component: RabatComponent, 69 | }, 70 | { 71 | path: "agadir", 72 | component: AgadirComponent, 73 | }, 74 | { 75 | path: "tangier", 76 | component: TangierComponent, 77 | }, 78 | { 79 | path: "marrakech", 80 | component: MarrakechComponent, 81 | }, 82 | { 83 | path: "signup", 84 | component: SignUpComponent, 85 | }, 86 | { 87 | path: "login", 88 | component: LoginComponent, 89 | }, 90 | ], 91 | }, 92 | { 93 | path: "explore", component: ExploreComponent, 94 | }, 95 | { path: "**", component: NotFoundComponent }, 96 | ]; 97 | 98 | -------------------------------------------------------------------------------- /src/app/layout/item/item.component.html: -------------------------------------------------------------------------------- 1 | @if (product) { 2 |
3 |
4 | product-image 5 |
6 | {{ product.title }} 7 |
8 |

9 | {{ product.desc }} 10 |

11 | 28 |
29 |
30 |
31 |
32 | Price Details 33 |
34 |
35 |
36 |
Min Price
37 |
{{ product.minprice }} MAD
38 |
39 |
40 |
Max Price
41 |
{{ product.maxprice }} MAD
42 |
43 |
44 |
City
45 | 46 | {{product.city}} 47 | 48 |
49 |
50 |
Category
51 |
52 | {{ product.category }} 53 |
54 |
55 |
56 |

57 | Please note that the displayed prices are for reference only and represent averages, not final or actual costs. 58 |

59 | 60 | Terms of use 61 | 62 |
63 |
64 |
65 |
66 |
Where to find this product:
67 |
68 | 71 |
72 | } 73 | @else { 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/app/components/input/input.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectorRef, 4 | Component, 5 | ElementRef, 6 | Input, 7 | OnInit, 8 | ViewChild, 9 | } from "@angular/core"; 10 | import { 11 | inputAddonClass, 12 | inputBaseClass, 13 | InputSize, 14 | inputSizeClasses, 15 | InputType, 16 | } from "./input.properties"; 17 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; 18 | 19 | @Component({ 20 | selector: "app-input", 21 | standalone: true, 22 | imports: [], 23 | templateUrl: "./input.component.html", 24 | providers: [ 25 | { 26 | provide: NG_VALUE_ACCESSOR, 27 | useExisting: InputComponent, 28 | multi: true, 29 | }, 30 | ], 31 | }) 32 | export class InputComponent 33 | implements OnInit, ControlValueAccessor, AfterViewInit 34 | { 35 | @Input() input: InputType = "text"; 36 | @Input() size: InputSize = "md"; 37 | @Input() label: string | null = null; 38 | @Input() inputId: string = ""; 39 | @Input() name: string = ""; 40 | @Input() placeholder: string = ""; 41 | @Input() options: { value: string; label: string }[] = []; 42 | @Input() multiple: boolean = false; 43 | @Input() required: boolean = false; 44 | @Input() preIcon: string | null = null; 45 | @Input() postIcon: string | null = null; 46 | @Input() addon: string | null = null; 47 | @Input() enablePasswordToggle: boolean = false; 48 | @Input() containerClass: string = ""; 49 | @Input() rows: number = 3; 50 | @Input() 51 | set disabled(disabled: boolean) { 52 | this.inputClasses = disabled 53 | ? this.inputClasses + " opacity-50 pointer-events-none" 54 | : this.inputClasses.replace("opacity-50 pointer-events-none", ""); 55 | } 56 | 57 | inputClasses = inputBaseClass.join(" "); 58 | addonClasses = inputAddonClass.join(" "); 59 | iconClasses = ""; 60 | showPassword = false; 61 | value: any; 62 | 63 | touched = false; 64 | onChange = (value: any) => {}; 65 | onTouched = () => {}; 66 | 67 | constructor( 68 | private cdRef: ChangeDetectorRef, 69 | private ref: ElementRef, 70 | ) {} 71 | 72 | ngOnInit(): void { 73 | // Add classes based on the input size 74 | this.inputClasses += " " + inputSizeClasses[this.size].join(" "); 75 | 76 | // Add classes for icons 77 | if (this.preIcon) this.inputClasses += " pl-10"; 78 | if (this.postIcon) this.inputClasses += " pr-10"; 79 | this.input == "textarea" 80 | ? (this.iconClasses += " py-2.5 items-start") 81 | : (this.iconClasses += " items-center"); 82 | 83 | // Add classes for addon 84 | if (this.addon) this.inputClasses += " rounded-l-none"; 85 | this.addon && this.input == "textarea" 86 | ? (this.addonClasses += " py-2.5 items-start") 87 | : (this.addonClasses += " items-center"); 88 | } 89 | 90 | ngAfterViewInit(): void { 91 | // Get addon via content projection 92 | const inputAddon = this.ref.nativeElement.querySelector( 93 | "[ngprojectas=input-addon]", 94 | ); 95 | if (!this.addon && inputAddon) { 96 | this.inputClasses += " rounded-l-none"; 97 | this.cdRef.detectChanges(); 98 | } 99 | } 100 | 101 | togglePasswordVisibility(): void { 102 | if (!this.enablePasswordToggle) return; 103 | 104 | this.showPassword = !this.showPassword; 105 | this.input = this.showPassword ? "text" : "password"; 106 | this.postIcon = this.showPassword ? "ri-eye-off-line" : "ri-eye-line"; 107 | } 108 | 109 | onInput(event: Event): void { 110 | this.markAsTouched(); 111 | if (this.disabled) return; 112 | 113 | if (this.input === "select" && this.multiple) { 114 | const target = event.target as HTMLSelectElement; 115 | const selectedOptions = Array.from(target.selectedOptions); 116 | this.value = selectedOptions.map((option) => option.value); 117 | } else { 118 | const target = event.target as HTMLInputElement; 119 | this.value = target.value; 120 | } 121 | 122 | this.onChange(this.value); 123 | } 124 | 125 | markAsTouched(): void { 126 | if (this.touched) return; 127 | 128 | this.onTouched(); 129 | this.touched = true; 130 | } 131 | 132 | writeValue(obj: any): void { 133 | this.value = obj; 134 | } 135 | 136 | registerOnChange(fn: any): void { 137 | this.onChange = fn; 138 | } 139 | 140 | registerOnTouched(fn: any): void { 141 | this.onTouched = fn; 142 | } 143 | 144 | setDisabledState?(isDisabled: boolean): void { 145 | this.disabled = isDisabled; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "safeguide": { 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-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": "dist/safeguide", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": [ 24 | "zone.js" 25 | ], 26 | "tsConfig": "tsconfig.app.json", 27 | "inlineStyleLanguage": "scss", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets", 31 | { 32 | "glob": "**/*", 33 | "input": "./node_modules/leaflet/dist/images", 34 | "output": "assets/" 35 | }, 36 | "src/manifest.webmanifest", 37 | "src/manifest.webmanifest" 38 | ], 39 | "styles": [ 40 | "src/styles.scss" 41 | ], 42 | "scripts": [], 43 | "server": "src/main.server.ts", 44 | "prerender": true, 45 | "ssr": { 46 | "entry": "server.ts" 47 | } 48 | }, 49 | "configurations": { 50 | "production": { 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "4mb", 55 | "maximumError": "5mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "2kb", 60 | "maximumError": "4kb" 61 | } 62 | ], 63 | "outputHashing": "all", 64 | "serviceWorker": "ngsw-config.json" 65 | }, 66 | "development": { 67 | "optimization": false, 68 | "extractLicenses": false, 69 | "sourceMap": true, 70 | "fileReplacements": [ 71 | { 72 | "replace": "src/environments/environment.ts", 73 | "with": "src/environments/environment.development.ts" 74 | } 75 | ] 76 | } 77 | }, 78 | "defaultConfiguration": "production" 79 | }, 80 | "serve": { 81 | "builder": "@angular-devkit/build-angular:dev-server", 82 | "configurations": { 83 | "production": { 84 | "buildTarget": "safeguide:build:production" 85 | }, 86 | "development": { 87 | "buildTarget": "safeguide:build:development" 88 | } 89 | }, 90 | "defaultConfiguration": "development" 91 | }, 92 | "extract-i18n": { 93 | "builder": "@angular-devkit/build-angular:extract-i18n", 94 | "options": { 95 | "buildTarget": "safeguide:build" 96 | } 97 | }, 98 | "test": { 99 | "builder": "@angular-devkit/build-angular:karma", 100 | "options": { 101 | "polyfills": [ 102 | "zone.js", 103 | "zone.js/testing" 104 | ], 105 | "tsConfig": "tsconfig.spec.json", 106 | "inlineStyleLanguage": "scss", 107 | "assets": [ 108 | "src/favicon.ico", 109 | "src/assets", 110 | "src/manifest.webmanifest", 111 | "src/manifest.webmanifest" 112 | ], 113 | "styles": [ 114 | "src/styles.scss" 115 | ], 116 | "scripts": [] 117 | } 118 | }, 119 | "deploy": { 120 | "builder": "@angular/fire:deploy", 121 | "options": { 122 | "version": 2 123 | }, 124 | "configurations": { 125 | "production": { 126 | "buildTarget": "safeguide:build:production", 127 | "serveTarget": "safeguide:serve:production" 128 | }, 129 | "development": { 130 | "buildTarget": "safeguide:build:development", 131 | "serveTarget": "safeguide:serve:development" 132 | } 133 | }, 134 | "defaultConfiguration": "production" 135 | } 136 | } 137 | } 138 | }, 139 | "cli": { 140 | "analytics": "31a2eb25-a7b8-4faf-9bc8-fa5973c67b6e" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/app/core/services/authentification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { 4 | Auth, 5 | GoogleAuthProvider, 6 | createUserWithEmailAndPassword, 7 | signInWithEmailAndPassword, 8 | signInWithPopup, 9 | updateProfile, 10 | } from "@angular/fire/auth"; 11 | import { User, UserSignup } from "../models/user"; 12 | import { 13 | Firestore, 14 | Timestamp, 15 | doc, 16 | getDoc, 17 | onSnapshot, 18 | setDoc, 19 | } from "@angular/fire/firestore"; 20 | import { Observable } from "rxjs"; 21 | import { 22 | Storage, 23 | getDownloadURL, 24 | ref, 25 | uploadBytes, 26 | } from "@angular/fire/storage"; 27 | import { StorageService } from '../data/storage.service'; 28 | import { AngularFireAuth } from '@angular/fire/compat/auth'; 29 | import { AngularFirestore } from '@angular/fire/compat/firestore'; 30 | @Injectable({ 31 | providedIn: 'root' 32 | }) 33 | export class AuthentificationService { 34 | 35 | constructor(private auth: Auth, 36 | private firestore: Firestore, 37 | private storage: StorageService) { } 38 | 39 | 40 | public async signUp(userData: UserSignup) { 41 | try { 42 | // Create user 43 | const userCredential = await createUserWithEmailAndPassword( 44 | this.auth, 45 | userData.email, 46 | userData.password, 47 | ); 48 | // Store user data in Firestore 49 | await this.storeUserData(userCredential.user.uid, userData); 50 | 51 | // Return user data 52 | return { 53 | error: null, 54 | user: userCredential.user, 55 | }; 56 | } catch (error: any) { 57 | return { 58 | error, 59 | user: null, 60 | }; 61 | } 62 | } 63 | private async storeUserData(userId: string, userData: UserSignup) { 64 | try { 65 | const userRef = doc(this.firestore, 'users', userId); 66 | await setDoc(userRef, { 67 | firstName: userData.firstName, 68 | lastName: userData.lastName, 69 | email: userData.email, 70 | }); 71 | } catch (error) { 72 | console.error('Error while storing user data:', error); 73 | throw error; 74 | } 75 | } 76 | public getUser(userId: string): Observable { 77 | return new Observable((observer) => { 78 | const userDoc = doc(this.firestore, "users", userId); 79 | const unsubscribe = onSnapshot(userDoc, (user) => { 80 | observer.next(user.data() as User); 81 | }); 82 | 83 | return () => unsubscribe(); 84 | }); 85 | } 86 | public async signIn(email: string, password: string) { 87 | try { 88 | // Sign in user 89 | const userCredential = await signInWithEmailAndPassword( 90 | this.auth, 91 | email, 92 | password, 93 | ); 94 | const userDoc = doc(this.firestore, "users", userCredential.user.uid); 95 | const userSnapshot = await getDoc(userDoc); 96 | let userData = userSnapshot.data() as User; 97 | this.storage.user = userData; 98 | // Return user data 99 | return { 100 | error: null, 101 | user: userCredential.user, 102 | }; 103 | } catch (error: any) { 104 | return { 105 | error, 106 | user: null, 107 | }; 108 | } 109 | } 110 | 111 | public async signInWithGoogle() { 112 | // Show Google sign in popup 113 | const provider = new GoogleAuthProvider(); 114 | const userCredential = await signInWithPopup(this.auth, provider); 115 | 116 | // Check if user has an email 117 | if (userCredential.user.email === null) { 118 | return { 119 | error: "Google sign in failed", 120 | user: null, 121 | }; 122 | } 123 | 124 | // Check if user exists in the database 125 | const userDoc = doc(this.firestore, "users", userCredential.user.uid); 126 | const userSnapshot = await getDoc(userDoc); 127 | let userData = userSnapshot.data() as User; 128 | 129 | if (!userSnapshot.exists()) { 130 | // Get first and last name from display name 131 | const [firstName, lastName] = userCredential.user.displayName?.split( 132 | " ", 133 | ) || ["", ""]; 134 | 135 | // Initialize user 136 | const user = this.initUser({ 137 | id: userCredential.user.uid, 138 | firstName, 139 | lastName, 140 | email: userCredential.user.email, 141 | picture: userCredential.user.photoURL || "assets/user.svg", 142 | }); 143 | 144 | // Save user to the database 145 | await setDoc(userDoc, user); 146 | userData = user; 147 | } 148 | 149 | // Return user data 150 | return { 151 | error: null, 152 | user: userData, 153 | }; 154 | } 155 | 156 | public signOut() { 157 | this.auth.signOut(); 158 | } 159 | public initUser(data: Partial): User { 160 | // Define default user 161 | const defaultUser: User = { 162 | id: "", 163 | firstName: "Unknown", 164 | lastName: "", 165 | picture: "assets/user.svg", 166 | email: "", 167 | lastLogin: Timestamp.now(), 168 | joinedAt: Timestamp.now(), 169 | }; 170 | 171 | // Return user with data 172 | return { 173 | ...defaultUser, 174 | ...data 175 | }; 176 | } 177 | 178 | public async getAccessToken() { 179 | const token = await this.auth.currentUser?.getIdToken(); 180 | return token; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/app/layout/discover/discover.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { InputComponent } from '../../components/input/input.component'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Router } from '@angular/router'; 5 | import { StorageService } from '../../core/data/storage.service'; 6 | 7 | @Component({ 8 | selector: 'app-discover', 9 | standalone: true, 10 | imports: [InputComponent, FormsModule], 11 | templateUrl: './discover.component.html', 12 | styleUrl: './discover.component.scss' 13 | }) 14 | export class DiscoverComponent { 15 | constructor(private router: Router, private storage: StorageService) { } 16 | searchQuery = ""; 17 | cities = [ 18 | { 19 | name: "Fez", 20 | stadium: 'https://upload.wikimedia.org/wikipedia/commons/1/1c/Complexe_sportif_de_F%C3%A8s.jpg', 21 | stadiumDesc: 'The Fez Sports complex (Arabic: المركب الرياضي لفاس) is a multi-purpose stadium in Fez, Morocco. It is used mostly for football matches and it also has athletics facilities, the stadium holds 45,000 and was built in 2003.', 22 | images: [ 23 | 'https://media.tacdn.com/media/attractions-splice-spp-674x446/06/e6/ee/72.jpg', 24 | 'https://i.natgeofe.com/n/f0823a24-c1f8-4c35-85b8-9c8afa25b0c4/dates-fez-morocco.jpg' 25 | ], 26 | image: "https://www.visitmorocco.com/sites/default/files/styles/thumbnail_destination_background_top5/public/thumbnails/image/tanneries-medina-of-fez-morocco-wizard8492.jpg?itok=VYYh3Kpd", 27 | desc: "Embark on a captivating journey through time in the heart of Morocco. Fes weaves an intricate tapestry of ancient medinas, historic wonders, and the rich cultural heritage that defines this enchanting city." 28 | }, 29 | { 30 | name: "Rabat", 31 | stadium: 'https://upload.wikimedia.org/wikipedia/commons/f/f2/Stade_Prince_Moulay_Abdellah.jpg', 32 | stadiumDesc: 'The Rabat Stadium, also known as the Prince Moulay Abdellah Stadium, is a multi-purpose stadium located in Rabat, Morocco. It is primarily used for football matches and serves as the home ground for the Moroccan national football team. The stadium has a seating capacity of around 52,000 spectators and is one of the largest stadiums in Morocco. It has hosted numerous international and domestic football events, including matches of the Moroccan league and national team fixtures. Additionally, it has also been used for other sporting events and concerts.', 33 | images: [ 34 | 'https://www.visitmorocco.com/sites/default/files/styles/thumbnail_destination_background_top5/public/thumbnails/image/tour-hassan-rabat-morocco-by-migel.jpg?itok=YP8GLwSi', 35 | 'https://www.visitmorocco.com/sites/default/files/styles/thumbnail_events_slider/public/thumbnails/image/rabat_1.jpg?itok=JF-8FRuT' 36 | ], 37 | image: "https://www.journalgeneraldeleurope.org/wp-content/uploads/2017/11/tour-hassan-rabat-maroc1.jpg", 38 | desc: "As the capital, Rabat gracefully balances tradition and modernity. Discover a regal city where historic monuments and contemporary vibes coexist in perfect harmony." 39 | }, 40 | { 41 | name: "Casablanca", 42 | stadiumDesc: 'The Casablanca Stadium, officially known as the Stade Mohammed V, is a prominent multi-purpose stadium located in Casablanca, Morocco. Named after King Mohammed V of Morocco, the stadium is one of the largest and most historic sports venues in the country. It serves primarily as the home ground for the major football clubs in Casablanca, including Raja Casablanca and Wydad Casablanca, two of Morocco most successful football teams. ', 43 | stadium: 'https://upload.wikimedia.org/wikipedia/commons/7/70/Stade_Mohamed_V%2C_Casablanca.jpg', 44 | images: [ 45 | 'https://images.ctfassets.net/bth3mlrehms2/1TwENu0ZXSnwNu6GzVfVE4/fa1176816167c1a03589cd613458585d/Marokko_Casablanca_Hassan_II_Moschee.jpg?w=3864&h=2173&fl=progressive&q=50&fm=jpg', 46 | 'https://www.maroc-hebdo.press.ma/files/2015/07/casa.jpg' 47 | ], 48 | image: "https://t3.ftcdn.net/jpg/02/67/20/10/360_F_267201056_wcEH6uQ6xu5oNHtY9Hq3YOhDwe1zk1XX.jpg", 49 | desc: "A vibrant fusion of historical charm and contemporary allure, where the echoes of the past harmonize with the dynamic pulse of modern life" 50 | }, 51 | { 52 | name: "Tangier", 53 | stadiumDesc: 'The Tangier Stadium, officially known as the Stade Ibn Batouta, is a major multi-purpose stadium located in Tangier, Morocco. Named after the famous Moroccan explorer Ibn Battuta, the stadium is one of the key sporting venues in the region. It serves primarily as the home ground for the local football club, Ittihad Riadi Tanger (IRT).\n The stadium has a seating capacity of around 45,000 spectators and has hosted numerous football matches and other sporting events. It is also a popular venue for concerts and other entertainment events. ', 54 | stadium: 'https://upload.wikimedia.org/wikipedia/commons/9/93/Stade_Ibn_Batuta%2C_Tanger.jpg', 55 | images: [ 56 | 'https://leseco.ma/wp-content/uploads/2021/01/Tanger.jpg', 57 | 'https://static.verychic.com/images/45838/en/desktop/hotel_marina_bay_17.jpg' 58 | ], 59 | image: "https://leseco.ma/wp-content/uploads/2021/01/Tanger.jpg", 60 | desc: "Positioned at the crossroads of two worlds, Tangier captivates with its unique blend of cultures and landscapes, where the Atlantic Ocean meets the Mediterranean Sea, creating a mesmerizing coastal panorama." 61 | }, 62 | { 63 | name: "Agadir", 64 | stadiumDesc: 'The Agadir Stadium, officially known as the Stade Adrar, is a prominent multi-purpose stadium located in Agadir, Morocco. It is one of the main sports venues in the city and serves primarily as the home ground for the local football club, Hassania Agadir. \n The Stade Adrar has a seating capacity of approximately 45,480 spectators. It has hosted various football matches, including domestic league games, cup competitions, and international fixtures. The stadium gained significant attention when it was one of the venues for the 2013 African Cup of Nations, where it hosted several matches during the tournament.', 65 | stadium: 'https://upload.wikimedia.org/wikipedia/commons/5/51/Stade-Adrar2019.png', 66 | images: [ 67 | 'https://www.visitmorocco.com/sites/default/files/styles/thumbnail_events_slider/public/thumbnails/image/AGADIR-%287%29.jpg?itok=ECYn9u_b', 68 | 'https://content.r9cdn.net/rimg/dimg/c0/3e/fc7d0d0d-city-35407-1650ff6f41f.jpg?width=1366&height=768&xhint=1569&yhint=1017&crop=true', 69 | 'https://cdn.getyourguide.com/img/tour/641e3943917fc.jpeg/145.jpg' 70 | ], 71 | image: "https://www.konouzimmobilier.com/wp-content/uploads/2019/12/951338247a403ff4a6be66bfe0f2e875.jpg", 72 | desc: "Nestled along golden shores, Agadir invites you to bask in the serenity of its beaches, offering a perfect blend of leisure and the gentle rhythm of coastal living." 73 | }, 74 | { 75 | name: "Marrakech", 76 | stadiumDesc: 'The Marrakech Stadium, officially known as the Stade de Marrakech, is a notable multi-purpose stadium located in Marrakech, Morocco. It is one of the primary sports venues in Marrakech and serves as the home ground for the local football club, Kawkab Marrakech. \n The Stade de Marrakech has a seating capacity of approximately 45,240 spectators. It has hosted numerous football matches, including domestic league games, cup competitions, and international fixtures. The stadium gained international attention when it was one of the venues for the 2014 FIFA Club World Cup, where several matches of the tournament took place at this venue.', 77 | stadium: 'https://upload.wikimedia.org/wikipedia/commons/0/0d/Stade_de_marrakech.jpg', 78 | images: [ 79 | 'https://www.marrakech-cityguide.com/wp-content/uploads/Marrakech-place-koutoubia-e1609154215571.jpg', 80 | 'https://www.clickexcursions.com/storage/excursions/June2022/9JVxSePAHAAf7SruQMH1.jpg' 81 | ], 82 | image: "https://fr.le360.ma/resizer/DWDGhyK5loODx8BUzM-7LvcUWOU=/1216x684/filters:format(jpg):quality(70)/cloudfront-eu-central-1.images.arcpublishing.com/le360/YZ7TV6HGPJB5JGSGMFQLVUXXXU.jpeg", 83 | desc: "A jewel in the south, Marrakech invites you to wander through vibrant souks, explore majestic palaces, and immerse yourself in the allure of a city where tradition dances with the rhythms of modern life." 84 | } 85 | ]; 86 | 87 | showDetails(city: any) { 88 | this.storage.city = city; 89 | console.log(city); 90 | this.router.navigate([`/${city.name}/infos`]); 91 | 92 | 93 | } 94 | 95 | } 96 | --------------------------------------------------------------------------------