diff --git a/android/app/build.gradle b/android/app/build.gradle index df4cf3b..bb0d83c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -3,7 +3,6 @@ apply plugin: 'com.android.application' android { namespace = "com.knots.mobile" compileSdk = rootProject.ext.compileSdkVersion - defaultConfig { applicationId "com.knots.mobile" minSdkVersion rootProject.ext.minSdkVersion @@ -11,19 +10,12 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - aaptOptions { - // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' } } - - // ✅ Forcer Java 17 - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - buildTypes { release { minifyEnabled false @@ -33,7 +25,7 @@ android { } repositories { - flatDir { + flatDir{ dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' } } @@ -59,4 +51,4 @@ try { } } catch(Exception e) { logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") -} \ No newline at end of file +} diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 5f41835..b296ba3 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ - Knots - Knots + knots + knots com.knots.mobile com.knots.mobile diff --git a/android/build.gradle b/android/build.gradle index b1d1d0c..f8f0e43 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - - allprojects { - gradle.projectsEvaluated { - tasks.withType(JavaCompile) { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - } - } - + repositories { google() mavenCentral() diff --git a/capacitor.config.ts b/capacitor.config.ts index 5e7c076..17717c3 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -2,8 +2,8 @@ import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'com.knots.mobile', - appName: 'Knots', - webDir: 'dist/knots-front/browser' + appName: 'knots', + webDir: 'dist' }; export default config; diff --git a/package-lock.json b/package-lock.json index ce0fa02..c76d3ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -247,7 +247,6 @@ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.23.tgz", "integrity": "sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "8.18.0", "ajv-formats": "3.0.1", @@ -284,7 +283,6 @@ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.23.tgz", "integrity": "sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==", "license": "MIT", - "peer": true, "dependencies": { "@angular-devkit/core": "19.2.23", "jsonc-parser": "3.3.1", @@ -432,7 +430,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.20.tgz", "integrity": "sha512-1M3W3FjUUbVKXDMs+yQpBhnkD/pCe0Jn79rPE5W+EGWWxFoLSyGX+fhnRO5m4c9k66p3nvYrikWQ0ZzMv3M5tw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -449,7 +446,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.20.tgz", "integrity": "sha512-LvjE8W58EACgTFaAoqmNe7FRsbvoQ0GvCB/rmm6AEMWx/0W/JBvWkQTrOQlwpoeYOHcMZRGdmPcZoUDwU3JySQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -463,7 +459,6 @@ "integrity": "sha512-tYYQk8AUz2sEkVl0a3uebduDUXPuKiGEKl2Jryrbn0xh9i1EsxoCjt1VvHnGnksGp3mz4DQihFVEnte0KeVQ5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -540,7 +535,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.20.tgz", "integrity": "sha512-pxzQh8ouqfE57lJlXjIzXFuRETwkfMVwS+NFCfv2yh01Qtx+vymO8ZClcJMgLPfBYinhBYX+hrRYVSa1nzlkRQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -557,7 +551,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.20.tgz", "integrity": "sha512-agi7InbMzop1jrud6L7SlNwnZk3iNolORcFIwBQMvKxLkcJ+ttbSYuM0KAw56IundWHf4dL9GP4cSygm4kUeFA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -576,7 +569,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.20.tgz", "integrity": "sha512-O9ZoQKILPC1T2c64OASS75XlOLBxY81m5AAgsBKhwiFWq+V28RsO0cnwpi1YSh/z4ryH8Fe7IUFz8jGrsJi3hQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -617,7 +609,6 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.20.tgz", "integrity": "sha512-y0fyKycxJHr82kxXKE50Vac5hPn5Kx3gw9CfqyEuwJ9VQzEixDljU+chrQK4Wods14jJn9Tt2ncNPGH1rLya3Q==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -662,7 +653,6 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -2519,7 +2509,6 @@ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz", "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -3213,7 +3202,6 @@ "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.1.2", "@inquirer/confirm": "^5.1.6", @@ -4965,7 +4953,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.2", "iterare": "1.2.1", @@ -7014,7 +7001,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7078,7 +7064,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7309,7 +7294,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", @@ -7635,7 +7619,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -8819,6 +8802,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -10563,7 +10569,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10705,7 +10710,6 @@ "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -12547,7 +12551,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -12955,8 +12958,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/regenerate": { "version": "1.4.2", @@ -13372,7 +13374,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -14431,7 +14432,6 @@ "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -14631,8 +14631,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "3.1.0", @@ -14688,7 +14687,6 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15094,7 +15092,6 @@ "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -15174,7 +15171,6 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -15770,8 +15766,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" } } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index af498c9..a1c9ba2 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -3,9 +3,10 @@ import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideIonicAngular } from '@ionic/angular/standalone'; -import {provideHttpClient} from "@angular/common/http"; +import {provideHttpClient, withInterceptors} from "@angular/common/http"; +import {authInterceptor} from "./core/auth/auth.interceptor"; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideIonicAngular({}), provideHttpClient()], + providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideIonicAngular({}),provideHttpClient(withInterceptors([authInterceptor])), provideHttpClient()], }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 0103fa8..59dbf38 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,4 +1,5 @@ import { Routes } from '@angular/router'; +import {authGuard} from "./core/auth/auth.guard"; export const routes: Routes = [ { @@ -8,35 +9,29 @@ export const routes: Routes = [ }, { path: 'main', + canActivate: [authGuard], loadComponent: () => import('./pages/main/main.component').then(x => x.Main), children: [ - /*{ - path:'groupmessages', - loadComponent: () => import('./pages/groupmessages/groupmessages.component').then(x => x.Groupmessages) - },*/ - { path: 'messages/:discussionId', loadComponent: () => import('./pages/messages/messages-main/messages-main.component').then(x => x.MessagesMain) }, { - path:'menu', + path: 'menu', loadComponent: () => import('./pages/menu/menu/menu.component').then(x => x.Menu) }, - { - path:'parameters', + path: 'parameters', loadComponent: () => import('./pages/parameters/parameters-main/parameters-main.component').then(x => x.ParametersMain) } ] }, { - path:'login', + path: 'login', loadComponent: () => import('./pages/login-form/login-form.component').then(x => x.LoginFormComponent) }, - { - path:'register', + path: 'register', loadComponent: () => import('./pages/register-form/register-form.component').then(x => x.RegisterFormComponent) }, ]; \ No newline at end of file diff --git a/src/app/core/auth/auth.guard.ts b/src/app/core/auth/auth.guard.ts new file mode 100644 index 0000000..cbbf02a --- /dev/null +++ b/src/app/core/auth/auth.guard.ts @@ -0,0 +1,13 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import {AuthService} from "./auth.service"; + +export const authGuard: CanActivateFn = () => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isLoggedIn()) return true; + + router.navigate(['/login']); + return false; +}; \ No newline at end of file diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts new file mode 100644 index 0000000..192feaf --- /dev/null +++ b/src/app/core/auth/auth.interceptor.ts @@ -0,0 +1,15 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import {AuthService} from "./auth.service"; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const token = inject(AuthService).getToken(); + + if (token) { + req = req.clone({ + setHeaders: { Authorization: `Bearer ${token}` } + }); + } + + return next(req); +}; \ No newline at end of file diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts new file mode 100644 index 0000000..ff61058 --- /dev/null +++ b/src/app/core/auth/auth.service.ts @@ -0,0 +1,68 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Router } from '@angular/router'; +import { firstValueFrom } from 'rxjs'; +import {LoggedUser} from "../../models/user.model"; + +interface LoginResponse { + token: string; + id: number; + username: string; + email: string | null; + tel: string | null; + profilePicture: string | null; + description: string | null; +} + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private http = inject(HttpClient); + private router = inject(Router); + + private readonly TOKEN_KEY = 'knots_token'; + private readonly USER_KEY = 'knots_user'; + + currentUser = signal(this.loadUser()); + + private loadUser(): LoggedUser | null { + const raw = localStorage.getItem(this.USER_KEY); + return raw ? JSON.parse(raw) : null; + } + + isLoggedIn(): boolean { + return !!localStorage.getItem(this.TOKEN_KEY); + } + + getToken(): string | null { + return localStorage.getItem(this.TOKEN_KEY); + } + + async login(username: string, password: string): Promise { + const response = await firstValueFrom( + this.http.post('/API/users/login', { username, password }) + ); + + localStorage.setItem(this.TOKEN_KEY, response.token); + + const user: LoggedUser = { + id: response.id, + username: response.username, + email: response.email, + tel: response.tel, + profilePicture: response.profilePicture, + description: response.description, + }; + + localStorage.setItem(this.USER_KEY, JSON.stringify(user)); + this.currentUser.set(user); + + await this.router.navigate(['/']); // adapte la route + } + + logout(): void { + localStorage.removeItem(this.TOKEN_KEY); + localStorage.removeItem(this.USER_KEY); + this.currentUser.set(null); + this.router.navigate(['/login']); + } +} \ No newline at end of file diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts new file mode 100644 index 0000000..56cf548 --- /dev/null +++ b/src/app/models/user.model.ts @@ -0,0 +1,8 @@ +export interface LoggedUser { + id: number; + username: string; + email: string | null; + tel: string | null; + profilePicture: string | null; + description: string | null; +} \ No newline at end of file diff --git a/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.css b/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.html b/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.html new file mode 100644 index 0000000..555a351 --- /dev/null +++ b/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.html @@ -0,0 +1,51 @@ + + + Modifier les coordonnées + + + + + + + + + +
+ + + + + + Adresse email invalide + + + + + + + Numéro à 10 chiffres requis + + + + Valider + + +
+
\ No newline at end of file diff --git a/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.ts b/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.ts new file mode 100644 index 0000000..73402cf --- /dev/null +++ b/src/app/pages/parameters/parameters-coordinates-form/parameters-coordinates-form.component.ts @@ -0,0 +1,88 @@ +import {Component, EventEmitter, inject, Output, signal} from '@angular/core'; +import { addIcons } from 'ionicons'; +import { closeOutline } from 'ionicons/icons'; +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, IonInput, + IonItem, IonNote, + IonTitle, + IonToolbar +} from "@ionic/angular/standalone"; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; +import {firstValueFrom} from "rxjs"; +import {KnotsDTOUserUpdateUserContactDto, UsersService} from "../../../services/api"; + +@Component({ + selector: 'app-parameters-coordinates-form', + imports: [ + IonHeader, + IonToolbar, + IonTitle, + IonButtons, + IonButton, + IonIcon, + IonContent, + ReactiveFormsModule, + IonItem, + IonInput, + IonNote + ], + templateUrl: './parameters-coordinates-form.component.html', + styleUrl: './parameters-coordinates-form.component.css' +}) +export class ParametersCoordinatesFormComponent { + @Output() close = new EventEmitter(); + + private userService = inject(UsersService); + //private authService = inject(AuthService); + + loading = signal(false); + + coordinatesForm = new FormGroup({ + email: new FormControl(null, [Validators.required, Validators.email]), + tel: new FormControl(null, [Validators.required, Validators.pattern(/^\d{10}$/)]), + }); + + constructor() { + addIcons({ closeOutline }); + } + + + + ngOnInit() { + //const user = this.userService.currentUser(); + + //this.coordinatesForm.patchValue({ + //email: user?.email ?? null, + //tel: user?.tel ?? null, + //}); + } + + get email() { return this.coordinatesForm.get('email')!; } + get tel() { return this.coordinatesForm.get('tel')!; } + + async submitForm() { + if (this.coordinatesForm.invalid) return; + this.loading.set(true); + + //const user = this.authService.currentUser(); + + const userValue: KnotsDTOUserUpdateUserContactDto = { + email: this.coordinatesForm.value.email, + tel: this.coordinatesForm.value.tel, + }; + + try { + //await firstValueFrom(this.userService.patchUserContactEndpoint(user.id, userValue)); + this.coordinatesForm.reset(); + this.close.emit(); + } catch (e) { + console.error('Erreur lors de la mise à jour des coordonnées', e); + } + + this.loading.set(false); + } +} diff --git a/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.html b/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.html index b2ef56f..4ddd0c2 100644 --- a/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.html +++ b/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.html @@ -1,4 +1,4 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.ts b/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.ts index 2099785..961b327 100644 --- a/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.ts +++ b/src/app/pages/parameters/parameters-coordinates/parameters-coordinates.component.ts @@ -1,43 +1,18 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {Component} from '@angular/core'; +import { IonModal, IonButton } from '@ionic/angular/standalone'; import {FormsModule} from "@angular/forms"; - -export interface Coordonnees { - phone: string; - email: string; -} +import {ParametersCoordinatesFormComponent} from "../parameters-coordinates-form/parameters-coordinates-form.component"; @Component({ selector: 'app-parameters-coordinates', - imports: [ - FormsModule - ], + imports: [IonModal, FormsModule, ParametersCoordinatesFormComponent], templateUrl: './parameters-coordinates.component.html', styleUrl: './parameters-coordinates.component.css' }) export class ParametersCoordinatesComponent { - @Input() isOpen: boolean = false; - @Output() closed = new EventEmitter(); - @Output() saved = new EventEmitter(); + isModalOpen = false; - form: Coordonnees = { phone: '', email: '' }; - phoneFocused = false; - emailFocused = false; - - close(): void { - this.closed.emit(); - } - - save(): void { - if (this.form.phone || this.form.email) { - this.saved.emit({ ...this.form }); - this.close(); - } - } - - onOverlayClick(event: MouseEvent): void { - if ((event.target as HTMLElement).classList.contains('modal-overlay')) { - this.close(); - } - } + openModal() { this.isModalOpen = true; } + closeModal() { this.isModalOpen = false; } } diff --git a/src/app/pages/parameters/parameters-profile/parameters-profile.component.css b/src/app/pages/parameters/parameters-profile/parameters-profile.component.css index 88660c5..b015cad 100644 --- a/src/app/pages/parameters/parameters-profile/parameters-profile.component.css +++ b/src/app/pages/parameters/parameters-profile/parameters-profile.component.css @@ -247,4 +247,10 @@ opacity: 1; transform: scale(1.15); } + + .field-error { + color: var(--ion-color-danger, #eb445a); + font-size: 0.75rem; + padding: 2px 0 0 4px; + } } \ No newline at end of file diff --git a/src/app/pages/parameters/parameters-profile/parameters-profile.component.html b/src/app/pages/parameters/parameters-profile/parameters-profile.component.html index 61d6c09..ef23de3 100644 --- a/src/app/pages/parameters/parameters-profile/parameters-profile.component.html +++ b/src/app/pages/parameters/parameters-profile/parameters-profile.component.html @@ -41,6 +41,9 @@ + + {{ usernameError() }} +
diff --git a/src/app/pages/parameters/parameters-profile/parameters-profile.component.ts b/src/app/pages/parameters/parameters-profile/parameters-profile.component.ts index 1b46cc9..f1aeb22 100644 --- a/src/app/pages/parameters/parameters-profile/parameters-profile.component.ts +++ b/src/app/pages/parameters/parameters-profile/parameters-profile.component.ts @@ -1,6 +1,9 @@ -import {Component, signal} from '@angular/core'; +import {Component, inject, signal} from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import {firstValueFrom} from "rxjs"; +import {UsersService} from "../../../services/api"; +import {AuthService} from "../../../core/auth/auth.service"; @Component({ @@ -10,36 +13,118 @@ import { FormsModule } from '@angular/forms'; styleUrl: './parameters-profile.component.css' }) export class ParametersProfileComponent { - username = signal('Doggeybag'); - bio = signal('Joueur Valorant'); + private usersService = inject(UsersService); + private authService = inject(AuthService); + + profileImage = signal(null); + username = signal(null); + bio = signal(null); editingUsername = signal(false); editingBio = signal(false); - profileImage = signal(null); + usernameError = signal(null); + loading = signal(false); - onPhotoChange(event: Event): void { - const input = event.target as HTMLInputElement; - if (input.files && input.files[0]) { - const reader = new FileReader(); - reader.onload = (e) => { - this.profileImage.set(e.target?.result as string); - }; - reader.readAsDataURL(input.files[0]); + ngOnInit() { + const user = this.authService.currentUser(); + this.profileImage.set(user?.profilePicture ?? null); + this.username.set(user?.username ?? null); + this.bio.set(user?.description ?? null); + } + + // --- Username --- + + toggleEditUsername() { + if (this.editingUsername()) { + this.submitUsername(); + } else { + this.usernameError.set(null); + this.editingUsername.set(true); } } - triggerFileInput(): void { - const input = document.getElementById('photoInput') as HTMLInputElement; - input?.click(); + async submitUsername() { + const value = this.username()?.trim(); + + if (!value) { + this.usernameError.set('Le pseudo ne peut pas être vide.'); + return; + } + if (value === this.authService.currentUser()?.username) { + this.editingUsername.set(false); + return; + } + + this.loading.set(true); + const user = this.authService.currentUser(); + + try { + await firstValueFrom(this.usersService.patchUsernameEndpoint(user.id, { username: value })); + this.usernameError.set(null); + this.editingUsername.set(false); + } catch (e: any) { + if (e?.status === 400) { + this.usernameError.set('Ce nom d\'utilisateur est déjà pris.'); + } else { + this.usernameError.set('Erreur lors de la mise à jour.'); + } + } + + this.loading.set(false); } - toggleEditUsername(): void { - this.editingUsername.update((v) => !v); + // --- Bio --- + + toggleEditBio() { + if (this.editingBio()) { + this.submitBio(); + } else { + this.editingBio.set(true); + } } - toggleEditBio(): void { - this.editingBio.update((v) => !v); + async submitBio() { + this.loading.set(true); + const user = this.authService.currentUser(); + + try { + await firstValueFrom(this.usersService.patchUserDescriptionEndpoint(user.id, { description: this.bio() })); + this.editingBio.set(false); + } catch (e) { + console.error('Erreur lors de la mise à jour de la bio', e); + } + + this.loading.set(false); + } + + // --- Photo --- + + triggerFileInput() { + document.getElementById('photoInput')?.click(); + } + + async onPhotoChange(event: Event) { + const file = (event.target as HTMLInputElement).files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async () => { + const base64 = reader.result as string; + this.profileImage.set(base64); + + this.loading.set(true); + const user = this.authService.currentUser(); + + try { + await firstValueFrom(this.usersService.patchUserProfilePictureEndpoint(user.id, { profilePicture: base64 })); + } catch (e) { + console.error('Erreur lors de la mise à jour de la photo', e); + } + + this.loading.set(false); + }; + reader.readAsDataURL(file); } } diff --git a/src/app/services/api/api/discussions.service.ts b/src/app/services/api/api/discussions.service.ts index 45e4bac..aacc2be 100644 --- a/src/app/services/api/api/discussions.service.ts +++ b/src/app/services/api/api/discussions.service.ts @@ -172,17 +172,29 @@ export class DiscussionsService extends BaseService { /** * @endpoint get /API/discussions * @param id + * @param name + * @param isGroup + * @param membersCount * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. * @param options additional options */ - public getDiscussionEndpoint(id: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; - public getDiscussionEndpoint(id: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; - public getDiscussionEndpoint(id: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; - public getDiscussionEndpoint(id: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { + public getDiscussionEndpoint(id: number, name: string, isGroup: boolean, membersCount: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; + public getDiscussionEndpoint(id: number, name: string, isGroup: boolean, membersCount: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public getDiscussionEndpoint(id: number, name: string, isGroup: boolean, membersCount: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public getDiscussionEndpoint(id: number, name: string, isGroup: boolean, membersCount: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { if (id === null || id === undefined) { throw new Error('Required parameter id was null or undefined when calling getDiscussionEndpoint.'); } + if (name === null || name === undefined) { + throw new Error('Required parameter name was null or undefined when calling getDiscussionEndpoint.'); + } + if (isGroup === null || isGroup === undefined) { + throw new Error('Required parameter isGroup was null or undefined when calling getDiscussionEndpoint.'); + } + if (membersCount === null || membersCount === undefined) { + throw new Error('Required parameter membersCount was null or undefined when calling getDiscussionEndpoint.'); + } let localVarQueryParameters = new OpenApiHttpParams(this.encoder); @@ -195,6 +207,33 @@ export class DiscussionsService extends BaseService { ); + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'Name', + name, + QueryParamStyle.Form, + true, + ); + + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'IsGroup', + isGroup, + QueryParamStyle.Form, + true, + ); + + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'MembersCount', + membersCount, + QueryParamStyle.Form, + true, + ); + + let localVarHeaders = this.defaultHeaders; const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ diff --git a/src/app/services/api/api/users.service.ts b/src/app/services/api/api/users.service.ts index 16b6eba..6ebc6cd 100644 --- a/src/app/services/api/api/users.service.ts +++ b/src/app/services/api/api/users.service.ts @@ -58,10 +58,10 @@ export class UsersService extends BaseService { * @param reportProgress flag to report request and response progress. * @param options additional options */ - public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext, transferCache?: boolean}): Observable; - public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext, transferCache?: boolean}): Observable>; - public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext, transferCache?: boolean}): Observable>; - public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext, transferCache?: boolean}): Observable { + public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public createUserEndpoint(knotsDTOUserCreateUserDto: KnotsDTOUserCreateUserDto, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (knotsDTOUserCreateUserDto === null || knotsDTOUserCreateUserDto === undefined) { throw new Error('Required parameter knotsDTOUserCreateUserDto was null or undefined when calling createUserEndpoint.'); } @@ -69,8 +69,7 @@ export class UsersService extends BaseService { let localVarHeaders = this.defaultHeaders; const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ - 'application/json', - 'application/problem+json' + 'application/json' ]); if (localVarHttpHeaderAcceptSelected !== undefined) { localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); diff --git a/src/app/services/api/chat.service.ts b/src/app/services/api/chat.service.ts deleted file mode 100644 index a4dd04d..0000000 --- a/src/app/services/api/chat.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HubConnection, HubConnectionBuilder, HubConnectionState } from '@microsoft/signalr'; - -@Injectable({ providedIn: 'root' }) -export class ChatService { - - private hub: HubConnection; - - constructor() { - this.hub = new HubConnectionBuilder() - .withUrl('https://localhost:5001/hubs/chat') - .withAutomaticReconnect() - .build(); - } - - async connect() { - if (this.hub.state === HubConnectionState.Disconnected) { - await this.hub.start(); - } - } - - async sendMessage(discussionId: string, content: string) { - await this.connect(); // s'assure que la connexion est active - await this.hub.invoke('SendMessage', discussionId, content); - } - - onMessage(callback: (message: any) => void) { - this.hub.on('ReceiveMessage', callback); - } -} \ No newline at end of file diff --git a/src/app/services/api/discussion.service.ts b/src/app/services/api/discussion.service.ts deleted file mode 100644 index 4c14d21..0000000 --- a/src/app/services/api/discussion.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Observable} from "rxjs"; -import {Discussion} from "../../pages/menu/menu-users/menu-users.component"; -import {HttpClient} from "@angular/common/http"; -import {inject, Injectable} from "@angular/core"; - -@Injectable({ providedIn: 'root' }) -export class discussionsService { - - private http = inject(HttpClient); - private apiUrl = 'https://localhost:5001/API'; - - getDiscussions(): Observable { - return this.http.get(`${this.apiUrl}/discussions`); - } -} \ No newline at end of file diff --git a/src/app/services/api/login.service.ts b/src/app/services/api/login.service.ts deleted file mode 100644 index 44fd835..0000000 --- a/src/app/services/api/login.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from "@angular/common/http"; -import { Observable, tap, finalize } from "rxjs"; - -export interface LoginRequest { - name: string; - password: string; -} - -export interface LoginResponse { - token: string; - discussionId: string; -} - -@Injectable({ - providedIn: 'root' -}) -export class LoginService { - - private http = inject(HttpClient); - private apiUrl = 'https://localhost:5001/API'; - private isRefreshing = false; - - login(credentials: LoginRequest): Observable { - return this.http.post(`${this.apiUrl}/auth/login`, credentials).pipe( - tap(response => localStorage.setItem('token', response.token)) - ); - } - - refreshToken(): Observable<{ token: string }> { - this.isRefreshing = true; - - return this.http.post<{ token: string }>(`${this.apiUrl}/auth/refresh`, {}).pipe( - tap(response => localStorage.setItem('token', response.token)), - finalize(() => { this.isRefreshing = false; }) - ); - } - - logout() { - localStorage.removeItem('token'); - } -} \ No newline at end of file diff --git a/src/app/services/api/model/models.ts b/src/app/services/api/model/models.ts index 54e4581..a9690cf 100644 --- a/src/app/services/api/model/models.ts +++ b/src/app/services/api/model/models.ts @@ -19,4 +19,4 @@ export * from './knots-dto-user-update-user-contact-dto'; export * from './knots-dto-user-update-user-description-dto'; export * from './knots-dto-user-update-user-password-dto'; export * from './knots-dto-user-update-user-profile-picture-dto'; -export * from './knots-dto-user-update-username-dto'; +export * from './knots-dto-user-update-username-dto'; \ No newline at end of file