feat : #1 ajout des parametres et gestion des informations personnelles publique
This commit is contained in:
2
CMD.md
2
CMD.md
@@ -1,6 +1,6 @@
|
||||
# commande Tree
|
||||
```bash
|
||||
tree -a -I 'node_modules|coverage|logs|.vscode|.git|dist|.git|.venv|__pycache__'
|
||||
tree -a -I 'node_modules|coverage|logs|.vscode|.git|dist|.git|.venv|__pycache__|.angular'
|
||||
```
|
||||
# Utilisation de `act` avec des workflows Gitea
|
||||
```bash
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<div
|
||||
class="app-container min-h-screen flex flex-col bg-white dark:bg-gray-900"
|
||||
[ngClass]="themeService.darkModeSignal()"
|
||||
>
|
||||
<div class="app-container min-h-screen flex flex-col bg-white dark:bg-gray-900">
|
||||
<!-- Navigation fixe -->
|
||||
<app-nav-bar class="flex-shrink-0" />
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
||||
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { mockSettingRepo } from '@app/testing/setting.mock';
|
||||
import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
@@ -12,6 +15,7 @@ describe('AppComponent', () => {
|
||||
|
||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
||||
let mockSettingRepository: jest.Mocked<Partial<SettingRepository>> = mockSettingRepo;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockAuthRepository = {
|
||||
@@ -39,6 +43,7 @@ describe('AppComponent', () => {
|
||||
provideRouter([]),
|
||||
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
||||
{ provide: SETTING_REPOSITORY_TOKEN, useValue: mockSettingRepository },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -54,10 +59,4 @@ describe('AppComponent', () => {
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have the 'TrouveTonProfil' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('TrouveTonProfil');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { NavBarComponent } from '@app/shared/components/nav-bar/nav-bar.component';
|
||||
import { FooterComponent } from '@app/shared/components/footer/footer.component';
|
||||
import { ThemeService } from '@app/core/services/theme/theme.service';
|
||||
import { SettingsFacade } from '@app/ui/settings/settings.facade';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -12,7 +12,10 @@ import { ThemeService } from '@app/core/services/theme/theme.service';
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'TrouveTonProfil';
|
||||
themeService = inject(ThemeService);
|
||||
export class AppComponent implements OnInit {
|
||||
private readonly settingsFacade = inject(SettingsFacade);
|
||||
|
||||
ngOnInit() {
|
||||
this.settingsFacade.loadSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth
|
||||
import { PbAuthRepository } from '@app/infrastructure/authentification/pb-auth.repository';
|
||||
import { WEB_SHARE_SERVICE_TOKEN } from '@app/infrastructure/shareData/web-share.service.token';
|
||||
import { WebShareService } from '@app/infrastructure/shareData/web-share.service';
|
||||
import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token';
|
||||
import { LocalSettingRepository } from '@app/infrastructure/settings/local-setting.repository';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@@ -45,6 +47,7 @@ export const appConfig: ApplicationConfig = {
|
||||
{ provide: USER_REPOSITORY_TOKEN, useExisting: PbUserRepository },
|
||||
{ provide: AUTH_REPOSITORY_TOKEN, useExisting: PbAuthRepository },
|
||||
{ provide: WEB_SHARE_SERVICE_TOKEN, useExisting: WebShareService },
|
||||
{ provide: SETTING_REPOSITORY_TOKEN, useClass: LocalSettingRepository },
|
||||
provideToastr({
|
||||
timeOut: 10000,
|
||||
positionClass: 'toast-top-right',
|
||||
|
||||
26
src/app/domain/settings/setting.model.ts
Normal file
26
src/app/domain/settings/setting.model.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export enum ThemeType {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
SYSTEM = 'system',
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
theme: ThemeType;
|
||||
privacy: {
|
||||
isProfilePublic: boolean;
|
||||
showEmail: boolean;
|
||||
showPhone: boolean;
|
||||
allowGeolocation: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Valeurs par défaut pour éviter les nulls
|
||||
export const DEFAULT_SETTINGS: UserSettings = {
|
||||
theme: ThemeType.SYSTEM,
|
||||
privacy: {
|
||||
isProfilePublic: false,
|
||||
showEmail: false,
|
||||
showPhone: false,
|
||||
allowGeolocation: false,
|
||||
},
|
||||
};
|
||||
8
src/app/domain/settings/setting.repository.ts
Normal file
8
src/app/domain/settings/setting.repository.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ThemeType, UserSettings } from '@app/domain/settings/setting.model';
|
||||
|
||||
export abstract class SettingRepository {
|
||||
abstract getSettings(): UserSettings;
|
||||
abstract saveSettings(settings: UserSettings): void;
|
||||
abstract applyTheme(settings: UserSettings): void;
|
||||
abstract getDefaultSystemTheme(): ThemeType;
|
||||
}
|
||||
37
src/app/infrastructure/settings/local-setting.repository.ts
Normal file
37
src/app/infrastructure/settings/local-setting.repository.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { DEFAULT_SETTINGS, ThemeType, UserSettings } from '@app/domain/settings/setting.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LocalSettingRepository implements SettingRepository {
|
||||
private readonly STORAGE_KEY = 'app_user_settings';
|
||||
|
||||
getSettings(): UserSettings {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
saveSettings(settings: UserSettings): void {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
applyTheme(settings: UserSettings): void {
|
||||
// Application immédiate de l'effet de bord du thème (Optionnel ici ou dans la Facade)
|
||||
if (settings.theme === ThemeType.DARK) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (settings.theme === ThemeType.SYSTEM) {
|
||||
document.documentElement.classList.add(this.getDefaultSystemTheme());
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultSystemTheme(): ThemeType {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? ThemeType.DARK
|
||||
: ThemeType.LIGHT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
|
||||
export const SETTING_REPOSITORY_TOKEN = new InjectionToken<SettingRepository>('SettingRepository');
|
||||
@@ -310,185 +310,195 @@
|
||||
<!-- Contenu principal avec onglets -->
|
||||
<main class="lg:col-span-3 animate-slide-up animation-delay-300">
|
||||
<!-- Navigation par onglets -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-4 mb-6">
|
||||
<nav class="flex flex-wrap gap-2">
|
||||
<button
|
||||
(click)="menu.set('settings')"
|
||||
[class.active-tab]="menu() === 'settings'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Paramètres</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('home')"
|
||||
[class.active-tab]="menu() === 'home'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
@if (missingFields()!.includes('nom')) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 flex-shrink-0 text-orange-500"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
<span class="hidden sm:inline">Mon profil</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('projects')"
|
||||
[class.active-tab]="menu() === 'projects'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Mes projets</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('update')"
|
||||
[class.active-tab]="menu() === 'update'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
@if (
|
||||
missingFields()!.includes("secteur d'activité") ||
|
||||
missingFields()!.includes('profession')
|
||||
) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 flex-shrink-0 text-orange-500"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
<span class="hidden sm:inline">Mes informations</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('cv')"
|
||||
[class.active-tab]="menu() === 'cv'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Lecteur PDF</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="menu_nav" />
|
||||
|
||||
<!-- Contenu des onglets -->
|
||||
<div class="tab-content">
|
||||
@switch (menu().toLowerCase()) {
|
||||
@case ('settings') {
|
||||
@if (!userLoading().isLoading) {
|
||||
<app-update-user [user]="user()" />
|
||||
} @else {
|
||||
<app-loading message="Chargement encours..." />
|
||||
}
|
||||
}
|
||||
@case ('home') {
|
||||
@if (!userLoading().isLoading) {
|
||||
<app-update-user [user]="user()" />
|
||||
} @else {
|
||||
<app-loading message="Chargement encours..." />
|
||||
}
|
||||
}
|
||||
@case ('projects') {
|
||||
<app-my-profile-project-list
|
||||
[projectIds]="profile().projets"
|
||||
[userId]="user().id"
|
||||
/>
|
||||
<router-outlet />
|
||||
}
|
||||
@case ('update') {
|
||||
<app-my-profile-update-form [profile]="profile()" />
|
||||
}
|
||||
@case ('cv') {
|
||||
<app-pdf-viewer [profile]="profile()" />
|
||||
}
|
||||
@default {
|
||||
@if (!userLoading().isLoading) {
|
||||
<app-update-user [user]="user()" />
|
||||
} @else {
|
||||
<app-loading message="Chargement encours..." />
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="menu_content" />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<ng-template #menu_nav>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-4 mb-6">
|
||||
<nav class="flex flex-wrap gap-2">
|
||||
<button
|
||||
(click)="menu.set('settings')"
|
||||
[class.active-tab]="menu() === 'settings'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Paramètres</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('home')"
|
||||
[class.active-tab]="menu() === 'home'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
@if (missingFields()!.includes('nom')) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 flex-shrink-0 text-orange-500"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
<span class="hidden sm:inline">Mon profil</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('projects')"
|
||||
[class.active-tab]="menu() === 'projects'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Mes projets</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('update')"
|
||||
[class.active-tab]="menu() === 'update'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
@if (
|
||||
missingFields()!.includes("secteur d'activité") || missingFields()!.includes('profession')
|
||||
) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 flex-shrink-0 text-orange-500"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
<span class="hidden sm:inline">Mes informations</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('cv')"
|
||||
[class.active-tab]="menu() === 'cv'"
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Lecteur PDF</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #menu_content>
|
||||
<div class="tab-content">
|
||||
@switch (menu().toLowerCase()) {
|
||||
@case ('settings') {
|
||||
<app-settings />
|
||||
}
|
||||
@case ('home') {
|
||||
@if (!userLoading().isLoading) {
|
||||
<app-update-user [user]="user()" />
|
||||
} @else {
|
||||
<app-loading message="Chargement encours..." />
|
||||
}
|
||||
}
|
||||
@case ('projects') {
|
||||
<app-my-profile-project-list [projectIds]="profile().projets" [userId]="user().id" />
|
||||
<router-outlet />
|
||||
}
|
||||
@case ('update') {
|
||||
<app-my-profile-update-form [profile]="profile()" />
|
||||
}
|
||||
@case ('cv') {
|
||||
<app-pdf-viewer [profile]="profile()" />
|
||||
}
|
||||
@default {
|
||||
@if (!userLoading().isLoading) {
|
||||
<app-update-user [user]="user()" />
|
||||
} @else {
|
||||
<app-loading message="Chargement encours..." />
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -13,6 +13,9 @@ import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||
import { mockToastR } from '@app/testing/toastr.mock';
|
||||
import { mockUserRepo } from '@app/testing/user.mock';
|
||||
import { mockAuthRepo } from '@app/testing/auth.mock';
|
||||
import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token';
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { mockSettingRepo } from '@app/testing/setting.mock';
|
||||
|
||||
describe('MyProfileComponent', () => {
|
||||
let component: MyProfileComponent;
|
||||
@@ -22,6 +25,7 @@ describe('MyProfileComponent', () => {
|
||||
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
|
||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>> = mockAuthRepo;
|
||||
let mockSettingRepository: jest.Mocked<Partial<SettingRepository>> = mockSettingRepo;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -31,6 +35,7 @@ describe('MyProfileComponent', () => {
|
||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
||||
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
|
||||
{ provide: SETTING_REPOSITORY_TOKEN, useValue: mockSettingRepository },
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, computed, inject, OnInit, signal } from '@angular/core';
|
||||
import { ActivatedRoute, RouterOutlet } from '@angular/router';
|
||||
import { User } from '@app/domain/users/user.model';
|
||||
import { Location, UpperCasePipe } from '@angular/common';
|
||||
import { Location, NgTemplateOutlet, UpperCasePipe } from '@angular/common';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { environment } from '@env/environment';
|
||||
import { ChipsComponent } from '@app/shared/components/chips/chips.component';
|
||||
@@ -13,6 +13,7 @@ import { PdfViewerComponent } from '@app/shared/features/pdf-viewer/pdf-viewer.c
|
||||
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
||||
import { UserFacade } from '@app/ui/users/user.facade';
|
||||
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
||||
import { SettingsComponent } from '@app/shared/features/settings/settings.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-profile',
|
||||
@@ -27,6 +28,8 @@ import { LoadingComponent } from '@app/shared/components/loading/loading.compone
|
||||
PdfViewerComponent,
|
||||
UpperCasePipe,
|
||||
LoadingComponent,
|
||||
SettingsComponent,
|
||||
NgTemplateOutlet,
|
||||
],
|
||||
providers: [UserFacade],
|
||||
templateUrl: './my-profile.component.html',
|
||||
|
||||
@@ -18,46 +18,6 @@
|
||||
|
||||
<!-- Actions utilisateur -->
|
||||
<div class="flex items-center gap-2 sm:gap-4">
|
||||
<!-- Toggle thème -->
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleDarkMode()"
|
||||
class="p-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
[attr.aria-label]="
|
||||
themeService.darkModeSignal() === 'dark'
|
||||
? 'Passer en mode clair'
|
||||
: 'Passer en mode sombre'
|
||||
"
|
||||
>
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (isAuthenticated() && isEmailVerified()) {
|
||||
<!-- Menu utilisateur connecté -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -66,92 +66,45 @@
|
||||
</svg>
|
||||
}
|
||||
@case ('github'.toLowerCase()) {
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="#FFFFFF"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="#181717"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
<svg
|
||||
class="w-6 h-6 dark:text-white"
|
||||
fill="#181717"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
@case ('x'.toLowerCase()) {
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="#FFFFFF"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>X</title>
|
||||
<path
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
fill="#000000"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>X</title>
|
||||
<path
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
<svg
|
||||
class="w-6 h-6 dark:text-white"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>X</title>
|
||||
<path
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
@case ('web'.toLowerCase()) {
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
<svg
|
||||
id="Layer_1"
|
||||
class="w-6 h-6"
|
||||
fill="#FFFFFF"
|
||||
data-name="Layer 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<title>world-globe-outline</title>
|
||||
<path
|
||||
d="M256,0Q362.11,0,436.9,75.1,512,149.89,512,256T436.9,436.9Q362.11,512,256,512T75.1,436.9Q0,362.11,0,256T75.1,75.1Q149.89,0,256,0ZM55.34,190.63a211.82,211.82,0,0,0,0,130.73h66a416,416,0,0,1,0-130.73Zm14.9,165.7q34.36,63.55,100.64,92.73a232.64,232.64,0,0,1-20.07-31.92,302.59,302.59,0,0,1-22.2-60.81ZM170.87,63.24Q104.59,92.43,70.54,155.37h58.07a304.36,304.36,0,0,1,22.2-60.51A198.45,198.45,0,0,1,170.87,63.24ZM151.72,190.63A390.59,390.59,0,0,0,145.94,256a390.48,390.48,0,0,0,5.78,65.37H241.1V190.63Zm82.7-143.51q-32.83,13.38-57.16,61.11-9.43,19.16-17.63,47.13H241.1V45.61a3.4,3.4,0,0,1-1.52.3h-1.82Zm3.34,419h1.82a5.12,5.12,0,0,0,1.52.3V356.33H159.62q8.21,28.28,17.63,47.43,24.32,47.74,57.16,61.11ZM274.24,45.91h-1.83a3.38,3.38,0,0,1-1.52-.3V155.37h81.48q-8.21-28-17.63-47.13-24.33-47.73-57.16-61.11Zm86,275.46A391.23,391.23,0,0,0,366.06,256a395,395,0,0,0-5.78-65.37H270.9V321.37Zm-82.7,143.51q32.84-13.39,57.16-61.11,9.73-19.16,17.63-47.43H270.9V466.39a5.1,5.1,0,0,0,1.52-.3h1.83ZM441.46,155.37q-34.06-62.94-100-92.12A212.61,212.61,0,0,1,361.2,94.86a295.22,295.22,0,0,1,22.2,60.51Zm-100,293.7q66-29.49,100-92.73H383.39q-8.52,33.74-22.2,60.81A226,226,0,0,1,341.43,449.06Zm49.25-258.43A412,412,0,0,1,395.86,256a415.71,415.71,0,0,1-5.17,65.37h66a211.89,211.89,0,0,0,0-130.73Z"
|
||||
/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
id="Layer_1"
|
||||
class="w-6 h-6"
|
||||
data-name="Layer 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<title>world-globe-outline</title>
|
||||
<path
|
||||
d="M256,0Q362.11,0,436.9,75.1,512,149.89,512,256T436.9,436.9Q362.11,512,256,512T75.1,436.9Q0,362.11,0,256T75.1,75.1Q149.89,0,256,0ZM55.34,190.63a211.82,211.82,0,0,0,0,130.73h66a416,416,0,0,1,0-130.73Zm14.9,165.7q34.36,63.55,100.64,92.73a232.64,232.64,0,0,1-20.07-31.92,302.59,302.59,0,0,1-22.2-60.81ZM170.87,63.24Q104.59,92.43,70.54,155.37h58.07a304.36,304.36,0,0,1,22.2-60.51A198.45,198.45,0,0,1,170.87,63.24ZM151.72,190.63A390.59,390.59,0,0,0,145.94,256a390.48,390.48,0,0,0,5.78,65.37H241.1V190.63Zm82.7-143.51q-32.83,13.38-57.16,61.11-9.43,19.16-17.63,47.13H241.1V45.61a3.4,3.4,0,0,1-1.52.3h-1.82Zm3.34,419h1.82a5.12,5.12,0,0,0,1.52.3V356.33H159.62q8.21,28.28,17.63,47.43,24.32,47.74,57.16,61.11ZM274.24,45.91h-1.83a3.38,3.38,0,0,1-1.52-.3V155.37h81.48q-8.21-28-17.63-47.13-24.33-47.73-57.16-61.11Zm86,275.46A391.23,391.23,0,0,0,366.06,256a395,395,0,0,0-5.78-65.37H270.9V321.37Zm-82.7,143.51q32.84-13.39,57.16-61.11,9.73-19.16,17.63-47.43H270.9V466.39a5.1,5.1,0,0,0,1.52-.3h1.83ZM441.46,155.37q-34.06-62.94-100-92.12A212.61,212.61,0,0,1,361.2,94.86a295.22,295.22,0,0,1,22.2,60.51Zm-100,293.7q66-29.49,100-92.73H383.39q-8.52,33.74-22.2,60.81A226,226,0,0,1,341.43,449.06Zm49.25-258.43A412,412,0,0,1,395.86,256a415.71,415.71,0,0,1-5.17,65.37h66a211.89,211.89,0,0,0,0-130.73Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
<svg
|
||||
id="Layer_1"
|
||||
class="w-6 h-6 dark:text-white text-black"
|
||||
data-name="Layer 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<title>world-globe-outline</title>
|
||||
<path
|
||||
d="M256,0Q362.11,0,436.9,75.1,512,149.89,512,256T436.9,436.9Q362.11,512,256,512T75.1,436.9Q0,362.11,0,256T75.1,75.1Q149.89,0,256,0ZM55.34,190.63a211.82,211.82,0,0,0,0,130.73h66a416,416,0,0,1,0-130.73Zm14.9,165.7q34.36,63.55,100.64,92.73a232.64,232.64,0,0,1-20.07-31.92,302.59,302.59,0,0,1-22.2-60.81ZM170.87,63.24Q104.59,92.43,70.54,155.37h58.07a304.36,304.36,0,0,1,22.2-60.51A198.45,198.45,0,0,1,170.87,63.24ZM151.72,190.63A390.59,390.59,0,0,0,145.94,256a390.48,390.48,0,0,0,5.78,65.37H241.1V190.63Zm82.7-143.51q-32.83,13.38-57.16,61.11-9.43,19.16-17.63,47.13H241.1V45.61a3.4,3.4,0,0,1-1.52.3h-1.82Zm3.34,419h1.82a5.12,5.12,0,0,0,1.52.3V356.33H159.62q8.21,28.28,17.63,47.43,24.32,47.74,57.16,61.11ZM274.24,45.91h-1.83a3.38,3.38,0,0,1-1.52-.3V155.37h81.48q-8.21-28-17.63-47.13-24.33-47.73-57.16-61.11Zm86,275.46A391.23,391.23,0,0,0,366.06,256a395,395,0,0,0-5.78-65.37H270.9V321.37Zm-82.7,143.51q32.84-13.39,57.16-61.11,9.73-19.16,17.63-47.43H270.9V466.39a5.1,5.1,0,0,0,1.52-.3h1.83ZM441.46,155.37q-34.06-62.94-100-92.12A212.61,212.61,0,0,1,361.2,94.86a295.22,295.22,0,0,1,22.2,60.51Zm-100,293.7q66-29.49,100-92.73H383.39q-8.52,33.74-22.2,60.81A226,226,0,0,1,341.43,449.06Zm49.25-258.43A412,412,0,0,1,395.86,256a415.71,415.71,0,0,1-5.17,65.37h66a211.89,211.89,0,0,0,0-130.73Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
}
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
import { ThemeService } from '@app/core/services/theme/theme.service';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
|
||||
@Component({
|
||||
@@ -13,5 +12,4 @@ import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
export class ReseauxComponent {
|
||||
@Input({ required: true }) reseaux: any = undefined;
|
||||
protected readonly Object = Object;
|
||||
protected themeService: ThemeService = inject(ThemeService);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ul class="flex flex-wrap gap-2 items-center justify-center">
|
||||
<li>
|
||||
<button
|
||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 transition-colors"
|
||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 transition-colors dark:text-white text-gray-800"
|
||||
[class.opacity-50]="currentPage === 1"
|
||||
[class.pointer-events-none]="currentPage === 1"
|
||||
[class.hover:bg-gray-100]="currentPage > 1"
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<li>
|
||||
<button
|
||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 transition-colors"
|
||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 transition-colors dark:text-white text-gray-800"
|
||||
[class.opacity-50]="currentPage >= filters.totalPages!"
|
||||
[class.pointer-events-none]="currentPage >= filters.totalPages!"
|
||||
[class.hover:bg-gray-100]="currentPage < filters.totalPages!"
|
||||
|
||||
225
src/app/shared/features/settings/settings.component.html
Normal file
225
src/app/shared/features/settings/settings.component.html
Normal file
@@ -0,0 +1,225 @@
|
||||
<section
|
||||
class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 py-8"
|
||||
>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Settings Form -->
|
||||
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" class="space-y-6">
|
||||
<!-- Theme Settings Card -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Apparence</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Thème
|
||||
</label>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
(click)="onThemeChange(ThemeType.LIGHT)"
|
||||
[class.ring-2]="settingsForm.get('theme')?.value === 'light'"
|
||||
class="relative flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-400 transition-all ring-indigo-500"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-yellow-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Clair</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="onThemeChange(ThemeType.DARK)"
|
||||
[class.ring-2]="settingsForm.get('theme')?.value === 'dark'"
|
||||
class="relative flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-400 transition-all ring-indigo-500"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-indigo-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Sombre</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="onThemeChange(ThemeType.SYSTEM)"
|
||||
[class.ring-2]="settingsForm.get('theme')?.value === 'system'"
|
||||
class="relative flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-indigo-500 dark:hover:border-indigo-400 transition-all ring-indigo-500"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Système</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Settings Card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden"
|
||||
formGroupName="privacy"
|
||||
>
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Confidentialité</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Profile Public Toggle -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<label class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Profil public
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Votre profil sera visible par tous les utilisateurs
|
||||
</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" formControlName="isProfilePublic" class="sr-only peer" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Show Email Toggle -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<label class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Afficher l'email
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Votre adresse email sera visible sur votre profil
|
||||
</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" formControlName="showEmail" class="sr-only peer" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Show Phone Toggle -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<label class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Afficher le téléphone
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Votre numéro de téléphone sera visible sur votre profil
|
||||
</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" formControlName="showPhone" class="sr-only peer" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Geolocation Toggle -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<label class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Activer la géolocalisation
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Votre position sera affichée sur la carte des profils
|
||||
</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" formControlName="allowGeolocation" class="sr-only peer" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-300 dark:peer-focus:ring-indigo-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
(click)="onCancel()"
|
||||
class="px-6 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="settingsForm.invalid"
|
||||
class="px-6 py-2.5 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
36
src/app/shared/features/settings/settings.component.spec.ts
Normal file
36
src/app/shared/features/settings/settings.component.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SettingsComponent } from './settings.component';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token';
|
||||
import { mockToastR } from '@app/testing/toastr.mock';
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { mockSettingRepo } from '@app/testing/setting.mock';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
|
||||
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||
let mockSettingRepository: jest.Mocked<Partial<SettingRepository>> = mockSettingRepo;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SettingsComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: SETTING_REPOSITORY_TOKEN, useValue: mockSettingRepository },
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
90
src/app/shared/features/settings/settings.component.ts
Normal file
90
src/app/shared/features/settings/settings.component.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Component, effect, inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ThemeType } from '@app/domain/settings/setting.model';
|
||||
import { SettingsFacade } from '@app/ui/settings/settings.facade';
|
||||
import { FeedbackService } from '@app/ui/shared/services/feedback.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule],
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrl: './settings.component.scss',
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
protected readonly ThemeType = ThemeType;
|
||||
private readonly settingsFacade = inject(SettingsFacade);
|
||||
private readonly fb: FormBuilder = inject(FormBuilder);
|
||||
private readonly feedbackService = inject(FeedbackService);
|
||||
|
||||
settings = this.settingsFacade.settings;
|
||||
|
||||
privacyForm = this.fb.group({
|
||||
isProfilePublic: [false, [Validators.required]],
|
||||
showEmail: [false, [Validators.required]],
|
||||
showPhone: [false, [Validators.required]],
|
||||
allowGeolocation: [false, [Validators.required]],
|
||||
});
|
||||
|
||||
settingsForm = this.fb.group({
|
||||
theme: [ThemeType.SYSTEM, [Validators.required]],
|
||||
privacy: this.privacyForm,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const userSettings = this.settings();
|
||||
if (userSettings) {
|
||||
this.settingsForm.patchValue(
|
||||
{
|
||||
theme: userSettings.theme,
|
||||
privacy: {
|
||||
isProfilePublic: userSettings.privacy.isProfilePublic,
|
||||
showEmail: userSettings.privacy.showEmail,
|
||||
showPhone: userSettings.privacy.showPhone,
|
||||
allowGeolocation: userSettings.privacy.allowGeolocation,
|
||||
},
|
||||
},
|
||||
{ emitEvent: false }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsFacade.loadSettings();
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.settingsForm.invalid) return;
|
||||
|
||||
const settingsFormValue = this.settingsForm.getRawValue();
|
||||
this.settingsFacade.updateSettings({
|
||||
...this.settings()!,
|
||||
theme: settingsFormValue.theme!,
|
||||
privacy: {
|
||||
isProfilePublic: !!settingsFormValue.privacy.isProfilePublic,
|
||||
showEmail: !!settingsFormValue.privacy.showEmail,
|
||||
showPhone: !!settingsFormValue.privacy.showPhone,
|
||||
allowGeolocation: !!settingsFormValue.privacy.allowGeolocation,
|
||||
},
|
||||
});
|
||||
|
||||
this.feedbackService.notify(null, 'Vos paramètres ont été enregistrés.', false);
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
const current = this.settings();
|
||||
if (current) {
|
||||
this.settingsForm.reset({
|
||||
theme: current.theme,
|
||||
privacy: current.privacy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onThemeChange(themeMode: ThemeType) {
|
||||
this.settingsForm.patchValue({ theme: themeMode });
|
||||
this.settingsFacade.applyThemeSettings({ ...this.settings(), theme: themeMode });
|
||||
}
|
||||
}
|
||||
8
src/app/testing/setting.mock.ts
Normal file
8
src/app/testing/setting.mock.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { of } from 'rxjs';
|
||||
import { UserSettings } from '@app/domain/settings/setting.model';
|
||||
|
||||
export const mockSettingRepo = {
|
||||
getSettings: jest.fn().mockReturnValue(of({} as UserSettings)),
|
||||
saveSettings: jest.fn(),
|
||||
applyTheme: jest.fn(),
|
||||
};
|
||||
69
src/app/ui/settings/settings.facade.ts
Normal file
69
src/app/ui/settings/settings.facade.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { ThemeType, UserSettings } from '@app/domain/settings/setting.model';
|
||||
import { GetSettingsUseCase } from '@app/usecase/settings/get-settings.usecase';
|
||||
import { UpdateSettingsUseCase } from '@app/usecase/settings/update-settings.usecase';
|
||||
import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token';
|
||||
import { ApplyThemeUsecase } from '@app/usecase/settings/apply-theme.usecase';
|
||||
import { GetDefaultSystemThemeUsecase } from '@app/usecase/settings/get-default-system-theme.usecase';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsFacade {
|
||||
private readonly settingRepository = inject(SETTING_REPOSITORY_TOKEN);
|
||||
|
||||
private readonly getSettingsUseCase = new GetSettingsUseCase(this.settingRepository);
|
||||
private readonly updateSettingsUseCase = new UpdateSettingsUseCase(this.settingRepository);
|
||||
private readonly applyThemeUseCase = new ApplyThemeUsecase(this.settingRepository);
|
||||
private readonly getDefaultSystemThemeUseCase = new GetDefaultSystemThemeUsecase(
|
||||
this.settingRepository
|
||||
);
|
||||
|
||||
readonly settings = signal<UserSettings>(this.getSettingsUseCase.execute());
|
||||
|
||||
constructor() {
|
||||
const initialSettings = {
|
||||
...this.settings(),
|
||||
theme: this.themeMode(),
|
||||
};
|
||||
this.applyThemeSettings(initialSettings);
|
||||
}
|
||||
|
||||
applyThemeSettings(initialSettings: UserSettings) {
|
||||
this.applyThemeUseCase.execute(initialSettings);
|
||||
}
|
||||
|
||||
loadSettings(): void {
|
||||
this.getSettingsUseCase.execute();
|
||||
}
|
||||
|
||||
updateSettings(settings: UserSettings): void {
|
||||
const settingReceived = {
|
||||
...this.settings(),
|
||||
theme: settings.theme,
|
||||
privacy: {
|
||||
isProfilePublic: settings.privacy.isProfilePublic,
|
||||
showEmail: settings.privacy.showEmail,
|
||||
showPhone: settings.privacy.showPhone,
|
||||
allowGeolocation: settings.privacy.allowGeolocation,
|
||||
},
|
||||
};
|
||||
this.updateSettingsUseCase.execute(settingReceived);
|
||||
this.applyThemeSettings(settingReceived);
|
||||
}
|
||||
|
||||
private themeMode(): ThemeType {
|
||||
let theme = '';
|
||||
|
||||
switch (this.settings().theme) {
|
||||
case 'light':
|
||||
theme = 'light';
|
||||
break;
|
||||
case 'dark':
|
||||
theme = 'dark';
|
||||
break;
|
||||
case 'system':
|
||||
theme = this.getDefaultSystemThemeUseCase.execute() ? 'dark' : 'light';
|
||||
break;
|
||||
}
|
||||
return theme as ThemeType;
|
||||
}
|
||||
}
|
||||
55
src/app/ui/shared/services/feedback.service.ts
Normal file
55
src/app/ui/shared/services/feedback.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ActionType } from '@app/domain/action-type.util';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class FeedbackService {
|
||||
private readonly toastr = inject(ToastrService);
|
||||
|
||||
private readonly TOAST_CONFIG = {
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing' as const,
|
||||
progressBar: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param action Le type d'action (UPDATE, CREATE...). Passer null pour une info générique.
|
||||
* @param message Le message à afficher.
|
||||
* @param hasError Si true, affiche une erreur générique.
|
||||
*/
|
||||
notify(action: ActionType | null, message: string, hasError: boolean = false): void {
|
||||
if (hasError) {
|
||||
this.handleError();
|
||||
} else {
|
||||
this.handleSuccess(action, message);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(): void {
|
||||
this.toastr.error(
|
||||
`Une erreur s'est produite, veuillez réessayer ultérieurement`,
|
||||
`Erreur`,
|
||||
this.TOAST_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
private handleSuccess(action: ActionType | null, message: string): void {
|
||||
let title = 'Succès';
|
||||
|
||||
switch (action) {
|
||||
case ActionType.UPDATE:
|
||||
title = 'Mise à jour';
|
||||
break;
|
||||
case ActionType.CREATE:
|
||||
title = 'Création';
|
||||
break;
|
||||
case ActionType.DELETE:
|
||||
title = 'Suppression';
|
||||
break;
|
||||
}
|
||||
|
||||
this.toastr.success(message, title, this.TOAST_CONFIG);
|
||||
}
|
||||
}
|
||||
10
src/app/usecase/settings/apply-theme.usecase.ts
Normal file
10
src/app/usecase/settings/apply-theme.usecase.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { UserSettings } from '@app/domain/settings/setting.model';
|
||||
|
||||
export class ApplyThemeUsecase {
|
||||
constructor(private settingRepository: SettingRepository) {}
|
||||
|
||||
execute(settings: UserSettings): void {
|
||||
return this.settingRepository.applyTheme(settings);
|
||||
}
|
||||
}
|
||||
10
src/app/usecase/settings/get-default-system-theme.usecase.ts
Normal file
10
src/app/usecase/settings/get-default-system-theme.usecase.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { ThemeType } from '@app/domain/settings/setting.model';
|
||||
|
||||
export class GetDefaultSystemThemeUsecase {
|
||||
constructor(private settingRepository: SettingRepository) {}
|
||||
|
||||
execute(): ThemeType {
|
||||
return this.settingRepository.getDefaultSystemTheme();
|
||||
}
|
||||
}
|
||||
10
src/app/usecase/settings/get-settings.usecase.ts
Normal file
10
src/app/usecase/settings/get-settings.usecase.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { UserSettings } from '@app/domain/settings/setting.model';
|
||||
|
||||
export class GetSettingsUseCase {
|
||||
constructor(private settingRepository: SettingRepository) {}
|
||||
|
||||
execute(): UserSettings {
|
||||
return this.settingRepository.getSettings();
|
||||
}
|
||||
}
|
||||
10
src/app/usecase/settings/update-settings.usecase.ts
Normal file
10
src/app/usecase/settings/update-settings.usecase.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SettingRepository } from '@app/domain/settings/setting.repository';
|
||||
import { UserSettings } from '@app/domain/settings/setting.model';
|
||||
|
||||
export class UpdateSettingsUseCase {
|
||||
constructor(private settingRepository: SettingRepository) {}
|
||||
|
||||
execute(settings: UserSettings): void {
|
||||
return this.settingRepository.saveSettings(settings);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user