configuration pocketbase terminé (#5)

# Conflicts:
#	.gitignore
This commit is contained in:
Styve Lioumba
2025-08-21 18:41:52 +02:00
committed by styve Lioumba
parent 1dc1109482
commit 4fb600b0cb
179 changed files with 23970 additions and 15135 deletions

View File

@@ -1,4 +1,4 @@
node_modules/
*.env
Dockerfile
package-lock.json
node_modules/
*.env
Dockerfile
package-lock.json

View File

@@ -1,16 +1,16 @@
name: Main Workflow
on:
push:
branches:
- main
pull_request:
branches:
- main
- dev
- feat/*
- fix/*
jobs:
run-tests:
uses: ./.github/workflows/tests/docker-build.yaml
name: Main Workflow
on:
push:
branches:
- main
pull_request:
branches:
- main
- dev
- feat/*
- fix/*
jobs:
run-tests:
uses: ./.github/workflows/tests/docker-build.yaml

View File

@@ -1,62 +1,62 @@
name: Docker Build Check
# Déclencheur pour chaque pull request
on:
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
steps:
# 1. Checkout du code source du dépôt
- name: Checkout code
uses: actions/checkout@v3
# 2. Configuration de QEMU pour le support multi-plateformes
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# 3. Configuration de Docker Buildx pour la construction multi-arch
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
# 4. Test de la construction du Dockerfile pour les architectures amd64 et arm64
- name: Build local container
uses: docker/build-push-action@v4
with:
tags: lastrea/trouvetonprofile:latest
push: false
platforms: linux/arm64
load: true
# 5. Vérification de la taille de l'image construite
- name: Check image size
run: docker images lastrea/trouvetonprofile:latest --format "{{.Size}}"
# 6. Scan de l'image Docker pour détecter les vulnérabilités
- name: Scan Image
uses: anchore/scan-action@v3
id: scan
with:
image: "lastrea/trouvetonprofile:latest"
fail-build: true
severity-cutoff: critical
output-format: sarif
- name: Upload Anchore Scan SARIF Report
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
# 7. Vérification si la construction et le scan ont réussi
- name: Build and Scan verification
if: ${{ success() }}
run: echo "Dockerfile built and scanned successfully!"
# 8. Envoi d'un message d'échec en cas d'erreur lors de la construction ou du scan
- name: Notify on failure
if: ${{ failure() }}
run: echo "The Dockerfile build or scan failed! Please review the changes."
name: Docker Build Check
# Déclencheur pour chaque pull request
on:
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
steps:
# 1. Checkout du code source du dépôt
- name: Checkout code
uses: actions/checkout@v3
# 2. Configuration de QEMU pour le support multi-plateformes
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# 3. Configuration de Docker Buildx pour la construction multi-arch
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
# 4. Test de la construction du Dockerfile pour les architectures amd64 et arm64
- name: Build local container
uses: docker/build-push-action@v4
with:
tags: lastrea/trouvetonprofile:latest
push: false
platforms: linux/arm64
load: true
# 5. Vérification de la taille de l'image construite
- name: Check image size
run: docker images lastrea/trouvetonprofile:latest --format "{{.Size}}"
# 6. Scan de l'image Docker pour détecter les vulnérabilités
- name: Scan Image
uses: anchore/scan-action@v3
id: scan
with:
image: "lastrea/trouvetonprofile:latest"
fail-build: true
severity-cutoff: critical
output-format: sarif
- name: Upload Anchore Scan SARIF Report
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
# 7. Vérification si la construction et le scan ont réussi
- name: Build and Scan verification
if: ${{ success() }}
run: echo "Dockerfile built and scanned successfully!"
# 8. Envoi d'un message d'échec en cas d'erreur lors de la construction ou du scan
- name: Notify on failure
if: ${{ failure() }}
run: echo "The Dockerfile build or scan failed! Please review the changes."

View File

@@ -1,33 +1,33 @@
# ETAPE 1: Construire l'application
FROM node:20-alpine AS node-builder
ARG APP_NAME=technostrea
ARG ENVIRONMENT=production
ARG NG_VERSION=18
ENV APP_NAME=$APP_NAME
ENV ENVIRONMENT=$ENVIRONMENT
ENV NG_VERSION=$NG_VERSION
WORKDIR /app
COPY package*.json ./
RUN npm cache clean --force
RUN npm install -g @angular/cli@$NG_VERSION
COPY . .
RUN npm install --legacy-peer-deps
RUN ng build --configuration=$ENVIRONMENT --output-path=dist/
# ETAPE 2: Héberger l'application sur un serveur web nginx
FROM nginx:1.26.0-alpine as server
EXPOSE 80
WORKDIR /usr/share/nginx/html
RUN rm -rf /usr/share/nginx/html/*
COPY --from=node-builder /app/dist/browser /usr/share/nginx/html
COPY --from=node-builder /app/nginx.conf /etc/nginx/conf.d/default.conf
# ETAPE 1: Construire l'application
FROM node:20-alpine AS node-builder
ARG APP_NAME=technostrea
ARG ENVIRONMENT=production
ARG NG_VERSION=18
ENV APP_NAME=$APP_NAME
ENV ENVIRONMENT=$ENVIRONMENT
ENV NG_VERSION=$NG_VERSION
WORKDIR /app
COPY package*.json ./
RUN npm cache clean --force
RUN npm install -g @angular/cli@$NG_VERSION
COPY . .
RUN npm install --legacy-peer-deps
RUN ng build --configuration=$ENVIRONMENT --output-path=dist/
# ETAPE 2: Héberger l'application sur un serveur web nginx
FROM nginx:1.26.0-alpine as server
EXPOSE 80
WORKDIR /usr/share/nginx/html
RUN rm -rf /usr/share/nginx/html/*
COPY --from=node-builder /app/dist/browser /usr/share/nginx/html
COPY --from=node-builder /app/nginx.conf /etc/nginx/conf.d/default.conf

View File

@@ -1,4 +1,4 @@
# TrouveTonProfile
## Description
Trouve Ton Profile est une plateforme innovante qui permet à chacun de trouver facilement des professionnels qualifiés dans tous les domaines de la vie quotidienne. Que tu cherches un électricien, un plombier, un développeur, ou tout autre expert, la plateforme te permet de parcourir rapidement des profils, de découvrir leurs compétences, et de sélectionner la personne idéale pour répondre à tes besoins. Conçue pour faciliter l'accès à des talents locaux et spécialisés, Trouve Ton Profile est l'outil incontournable pour simplifier les tâches du quotidien en connectant les personnes à des solutions fiables et adaptées.
# TrouveTonProfile
## Description
Trouve Ton Profile est une plateforme innovante qui permet à chacun de trouver facilement des professionnels qualifiés dans tous les domaines de la vie quotidienne. Que tu cherches un électricien, un plombier, un développeur, ou tout autre expert, la plateforme te permet de parcourir rapidement des profils, de découvrir leurs compétences, et de sélectionner la personne idéale pour répondre à tes besoins. Conçue pour faciliter l'accès à des talents locaux et spécialisés, Trouve Ton Profile est l'outil incontournable pour simplifier les tâches du quotidien en connectant les personnes à des solutions fiables et adaptées.

View File

@@ -1,118 +1,98 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"TrouveTonProfile": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/trouve-ton-profile",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/primeicons/primeicons.css",
"src/styles.scss"
],
"scripts": [],
"server": "src/main.server.ts",
"prerender": false,
"ssr": {
"entry": "server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "TrouveTonProfile:build:production"
},
"development": {
"buildTarget": "TrouveTonProfile:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "TrouveTonProfile:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": "dda3ec82-e13e-4042-ae63-71d138479518"
}
}
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"TrouveTonProfile": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/primeicons/primeicons.css",
"node_modules/ngx-toastr/toastr.css",
"src/styles.scss"
],
"scripts": [],
"server": "src/main.server.ts",
"prerender": false,
"ssr": false
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "TrouveTonProfile:build:production"
},
"development": {
"buildTarget": "TrouveTonProfile:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "TrouveTonProfile:build"
}
}
}
}
},
"cli": {
"analytics": "dda3ec82-e13e-4042-ae63-71d138479518"
}
}

22
compose.yaml Normal file
View File

@@ -0,0 +1,22 @@
volumes:
pb_data:
networks:
ttp-net:
services:
pocketbase:
image: ghcr.io/muchobien/pocketbase:latest
container_name: pocketbase
restart: unless-stopped
ports:
- 8090:8090
networks:
- ttp-net
volumes:
- pb_data:/pb_data
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8090/api/health || exit 1
interval: 5s
timeout: 5s
retries: 5

16
jest.config.js Normal file
View File

@@ -0,0 +1,16 @@
module.exports = {
preset: 'jest-preset-angular',
rootDir: '.',
setupFilesAfterEnv: ['<rootDir>/src/setup-jest.ts'],
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/'],
transform: {
'^.+\\.ts$': 'ts-jest', // Only transform .ts files
},
transformIgnorePatterns: [
'/node_modules/(?!flat)/', // Exclude modules except 'flat' from transformation
],
moduleNameMapper: {
'^@app/(.*)$': '<rootDir>/src/app/$1',
'^@env/(.*)$': '<rootDir>/src/environments/$1',
},
};

View File

@@ -1,8 +1,8 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
}
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
}

31417
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +1,62 @@
{
"name": "trouve-ton-profile",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:TrouveTonProfile": "node dist/trouve-ton-profile/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/platform-server": "^17.0.0",
"@angular/router": "^17.0.0",
"@angular/ssr": "^17.0.1",
"@fortawesome/angular-fontawesome": "^0.14.1",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"angularx-qrcode": "^17.0.1",
"express": "^4.18.2",
"primeicons": "^7.0.0",
"primeng": "^17.18.10",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.1",
"@angular/cli": "^17.0.1",
"@angular/compiler-cli": "^17.0.0",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
"autoprefixer": "^10.4.20",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.12",
"typescript": "~5.2.2"
}
}
{
"name": "trouve-ton-profile",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start:dev": "docker compose up -d && ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"serve:ssr:TrouveTonProfile": "node dist/trouve-ton-profile/server/server.mjs"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/platform-server": "^17.0.0",
"@angular/router": "^17.0.0",
"@angular/ssr": "^17.0.1",
"@fortawesome/angular-fontawesome": "^0.14.1",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@ngneat/until-destroy": "^10.0.0",
"angularx-qrcode": "^17.0.1",
"express": "^4.18.2",
"ng2-pdf-viewer": "^10.3.3",
"ngx-toastr": "^17.0.2",
"pocketbase": "^0.21.5",
"primeicons": "^7.0.0",
"primeng": "^17.18.10",
"punycode": "^2.3.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.1",
"@angular/cli": "^17.0.1",
"@angular/compiler-cli": "^17.0.0",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/jest": "^30.0.0",
"@types/node": "^18.18.0",
"autoprefixer": "^10.4.20",
"jasmine-core": "~5.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^14.6.1",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.12",
"typescript": "~5.2.2"
}
}

View File

@@ -1,7 +1,7 @@
<main class="bg-white dark:bg-gray-900" [ngClass]="themeService.darkModeSignal()">
<app-nav-bar/>
<section class="content bg-white dark:bg-gray-900">
<router-outlet></router-outlet>
</section>
<app-footer/>
</main>
<main class="bg-white dark:bg-gray-900" [ngClass]="themeService.darkModeSignal()">
<app-nav-bar/>
<section class="content bg-white dark:bg-gray-900">
<router-outlet></router-outlet>
</section>
<app-footer/>
</main>

View File

@@ -1,18 +1,21 @@
main {
display: flex;
flex-direction: column;
min-height: 100vh; /* Minimum 100% de la hauteur de la fenêtre */
}
.content {
flex: 1; /* Cette zone grandit pour remplir l'espace restant */
padding: 20px;
}
app-nav-bar {
flex-shrink: 0; /* Le header ne doit pas rétrécir */
}
app-footer {
flex-shrink: 0; /* Le footer ne doit pas rétrécir */
}
main {
display: flex;
flex-direction: column;
min-height: 100vh; /* Minimum 100% de la hauteur de la fenêtre */
}
.content {
flex: 1; /* Cette zone grandit pour remplir l'espace restant */
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
app-nav-bar {
flex-shrink: 0; /* Le header ne doit pas rétrécir */
}
app-footer {
flex-shrink: 0; /* Le footer ne doit pas rétrécir */
}

View File

@@ -1,10 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import {provideRouter} from "@angular/router";
import {ThemeService} from "@app/core/services/theme/theme.service";
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
providers: [
provideRouter([])
],
}).compileComponents();
});
@@ -20,10 +26,4 @@ describe('AppComponent', () => {
expect(app.title).toEqual('TrouveTonProfile');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, TrouveTonProfile');
});
});

View File

@@ -1,18 +1,18 @@
import {Component, inject} 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';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, NavBarComponent, FooterComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'TrouveTonProfile';
themeService = inject(ThemeService);
}
import {Component, inject} 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';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, NavBarComponent, FooterComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'TrouveTonProfile';
themeService = inject(ThemeService);
}

View File

@@ -10,6 +10,7 @@ import {
import {routes} from './app.routes';
import {provideHttpClient, withFetch} from "@angular/common/http";
import {provideAnimations} from "@angular/platform-browser/animations";
import {provideToastr} from "ngx-toastr";
export const appConfig: ApplicationConfig = {
providers: [
@@ -24,6 +25,11 @@ export const appConfig: ApplicationConfig = {
}
)),
provideAnimations(),
provideHttpClient(withFetch())
provideHttpClient(withFetch()),
provideToastr({
timeOut: 10000,
positionClass: 'toast-top-right',
preventDuplicates: true,
}), // Toastr providers
]
};

View File

@@ -1,36 +1,38 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
title: 'Accueil',
loadChildren: () => import('@app/routes/home/home.module').then(m => m.HomeModule)
},
{
path: 'home',
title: 'Accueil',
loadChildren: () => import('@app/routes/home/home.module').then(m => m.HomeModule)
},
{
path: 'profiles',
title: 'Liste des profiles',
loadChildren: () => import('@app/routes/profile/profile.module').then(m => m.ProfileModule)
},
{
path:'auth',
title: 'Authentification',
loadChildren: () => import('@app/routes/authentification/authentification.module').then(m => m.AuthentificationModule)
},
{
path: 'my-profile',
title: 'Mon profile',
loadChildren: () => import('@app/routes/my-profile/my-profile.module').then(m => m.MyProfileModule)
},
{
path: 'not-found',
title: 'Page non trouvée',
loadChildren: () => import('@app/routes/not-found/not-found.module').then(m => m.NotFoundModule)
},
{path: '', redirectTo: '/', pathMatch: 'full'},
{path: '**', redirectTo: '/not-found'}
];
import {Routes} from '@angular/router';
import {authGuard} from "@app/core/guard/authentication/auth.guard";
export const routes: Routes = [
{
path: '',
title: 'Accueil',
loadChildren: () => import('@app/routes/home/home.module').then(m => m.HomeModule)
},
{
path: 'home',
title: 'Accueil',
loadChildren: () => import('@app/routes/home/home.module').then(m => m.HomeModule)
},
{
path: 'profiles',
title: 'Liste des profiles',
loadChildren: () => import('@app/routes/profile/profile.module').then(m => m.ProfileModule)
},
{
path: 'auth',
title: 'Authentification',
loadChildren: () => import('@app/routes/authentification/authentification.module').then(m => m.AuthentificationModule)
},
{
path: 'my-profile',
title: 'Mon profile',
canActivate: [authGuard],
loadChildren: () => import('@app/routes/my-profile/my-profile.module').then(m => m.MyProfileModule)
},
{
path: 'not-found',
title: 'Page non trouvée',
loadChildren: () => import('@app/routes/not-found/not-found.module').then(m => m.NotFoundModule)
},
{path: '', redirectTo: '/', pathMatch: 'full'},
{path: '**', redirectTo: '/not-found'}
];

View File

@@ -0,0 +1,58 @@
import {TestBed} from '@angular/core/testing';
import {authGuard} from './auth.guard';
import {AuthService} from "@app/core/services/authentication/auth.service";
import {signal} from "@angular/core";
import {Auth} from "@app/shared/models/auth";
import {CanActivateFn, Router} from '@angular/router';
describe('authGuard', () => {
let mockAuthService: Partial<AuthService>;
let mockRouter: Partial<Router>;
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
beforeEach(() => {
mockAuthService= {
user: signal<Auth | undefined>({ isValid: true, token: 'mockToken', record: null })
}
mockRouter ={
parseUrl: jest.fn()
}
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: mockAuthService },
{ provide: Router, useValue: mockRouter }
]
});
});
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
it('should allow access if user is valid', () => {
const mockRoute = {} as any;
const mockState = {} as any;
const result =TestBed.runInInjectionContext(() => executeGuard(mockRoute, mockState));
expect(result).toEqual(true);
});
it('should redirect to /auth if user is not valid', () => {
mockAuthService.user!.set({ isValid: false, token: '', record: null });
const mockRoute = {} as any;
const mockState = {} as any;
(mockRouter.parseUrl as jest.Mock).mockReturnValue('/auth');
const result = TestBed.runInInjectionContext(() => executeGuard(mockRoute, mockState));
expect(result).toEqual("/auth" as any);
expect(mockRouter.parseUrl).toHaveBeenCalledWith("/auth");
});
});

View File

@@ -0,0 +1,13 @@
import {CanActivateFn, Router} from '@angular/router';
import {inject} from "@angular/core";
import {AuthService} from "@app/core/services/authentication/auth.service";
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.user()!.isValid) {
return router.parseUrl("/auth")
}
return true;
};

View File

@@ -0,0 +1,52 @@
import {TestBed} from '@angular/core/testing';
import {Router} from '@angular/router';
import {myProfileResolver} from './my-profile.resolver';
import {User} from "@app/shared/models/user";
describe('myProfileResolver', () => {
let mockRouter: Partial<Router>;
beforeEach(() => {
mockRouter = {
getCurrentNavigation: jest.fn()
};
TestBed.configureTestingModule({
providers: [
{ provide: Router, useValue: mockRouter }
]
});
});
it('should return the user from navigation extras state', () => {
const user: User = {
id: 'adbc123',
username: "john_doe",
verified: true,
emailVisibility: false,
email: "jd@example.com",
created: new Date().toString(),
updated: new Date().toString(),
name: "john doe",
avatar: ""
};
// Mocke la méthode getCurrentNavigation pour retourner un objet attendu
(mockRouter.getCurrentNavigation as jest.Mock).mockReturnValue({
extras: {
state: {
user
}
}
});
const result = TestBed.runInInjectionContext(() => myProfileResolver(null as any, null as any));
expect(result).toEqual({ user });
expect(mockRouter.getCurrentNavigation).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,9 @@
import {ResolveFn, Router} from '@angular/router';
import {User} from "@app/shared/models/user";
import {inject} from "@angular/core";
export const myProfileResolver: ResolveFn<{ user: User }> = (route, state) => {
const router = inject(Router);
const user: User = router.getCurrentNavigation()!.extras.state!['user'] as User;
return {user}
};

View File

@@ -1,17 +1,67 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { detailResolver } from './detail.resolver';
describe('detailResolver', () => {
const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => detailResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
import {TestBed} from '@angular/core/testing';
import {Router} from '@angular/router';
import {detailResolver} from './detail.resolver';
import {User} from "@app/shared/models/user";
import {Profile} from "@app/shared/models/profile";
describe('detailResolver', () => {
let mockRoute: Partial<Router>;
beforeEach(() => {
mockRoute = {
getCurrentNavigation: jest.fn()
};
TestBed.configureTestingModule({
providers: [
{ provide: Router, useValue: mockRoute }
]
});
});
it('should return user and profile', () => {
const mockUser : User = {
id: 'adbc123',
username: "john_doe",
verified: true,
emailVisibility: false,
email: "jd@example.com",
created: new Date().toString(),
updated: new Date().toString(),
name: "john doe",
avatar: ""
};
const mockProfile : Profile = {
id: "string",
created: "string",
updated: "string",
profession: "string",
utilisateur: "string",
estVerifier: false,
secteur: "string",
reseaux: JSON.parse("{}"),
bio: "string",
cv: "string",
projets: [],
apropos: "string"
};
const fakeState = {} as any;
const fakeParams = { params: { name: 'john_doe'} } as any;
(mockRoute.getCurrentNavigation as jest.Mock).mockReturnValue({
extras: {
state: { user: mockUser, profile: mockProfile }
}
});
const result = TestBed.runInInjectionContext(() => detailResolver( fakeParams, fakeState));
expect(result).toEqual({ user: mockUser, profile: mockProfile });
expect(mockRoute.getCurrentNavigation).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,10 @@
import { ResolveFn } from '@angular/router';
export const detailResolver: ResolveFn<string> = (route, state) => {
const paramValue = route.params['name'];
return "profile-list-resolver works!, paramValue: " + paramValue || "no query value found!";
};
import {ResolveFn, Router} from '@angular/router';
import {inject} from "@angular/core";
import {User} from "@app/shared/models/user";
import {Profile} from "@app/shared/models/profile";
export const detailResolver: ResolveFn<{ user:User,profile:Profile }> = (route, state) => {
const paramValue = route.params['name'];
const router = inject(Router);
return router.getCurrentNavigation()?.extras.state as { user:User,profile:Profile }
};

View File

@@ -1,17 +1,41 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { listResolver } from './list.resolver';
describe('listResolver', () => {
const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => listResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
import { TestBed } from '@angular/core/testing';
import {ResolveFn, Router} from '@angular/router';
import { listResolver } from './list.resolver';
import {Profile} from "@app/shared/models/profile";
import {myProfileResolver} from "@app/core/resolvers/my-profile/my-profile.resolver";
import {ProfileService} from "@app/core/services/profile/profile.service";
import {Observable, of} from "rxjs";
describe('listResolver', () => {
let mockProfileService: Partial<ProfileService>;
beforeEach(() => {
mockProfileService = {
profiles: of([] as Profile[])
};
TestBed.configureTestingModule({
providers: [
{ provide: ProfileService, useValue: mockProfileService }
]
});
});
it('should return profiles observable from ProfileService', () => {
const fakeRoute ={ queryParams: { search: '' } } as any;
const fakeState = {} as any;
const expectedProfiles = [] as Profile[];
const result$ : Observable<Profile[]> = TestBed.runInInjectionContext(() => listResolver(fakeRoute, fakeState) as Observable<Profile[]>);
result$.subscribe((result:any) => {
expect(result).toEqual(expectedProfiles);
expect(mockProfileService.profiles).toBeDefined();
});
});
});

View File

@@ -1,6 +1,12 @@
import { ResolveFn } from '@angular/router';
export const listResolver: ResolveFn<string> = (route, state) => {
const queryValue = route.queryParams['search'];
return "profile-list-resolver works!, queryValue: " + queryValue || "no query value found!";
};
import { ResolveFn } from '@angular/router';
import {inject} from "@angular/core";
import {ProfileService} from "@app/core/services/profile/profile.service";
import {Observable} from "rxjs";
import {Profile} from "@app/shared/models/profile";
export const listResolver: ResolveFn<Observable<Profile[]>> = (route, state) => {
const queryValue = route.queryParams['search'];
const profileService = inject(ProfileService);
return profileService.profiles;
};

View File

@@ -0,0 +1,76 @@
import {TestBed} from '@angular/core/testing';
import {AuthService} from './auth.service';
import {LoginDto} from "@app/shared/models/login-dto";
import {RegisterDto} from "@app/shared/models/register-dto";
import {User} from "@app/shared/models/user";
describe('AuthService', () => {
let authService: AuthService;
let mockLoginUser : LoginDto = {email: 'john_doe@example.com', password: 'mysecretpassword'};
let mockRegisterUser: RegisterDto = {email:'john_doe@example.com', password: 'mysecretpassword', passwordConfirm: 'mysecretpassword', emailVisibility: false};
const mockAuth = {
isValid: false,
record: {email: mockLoginUser.email, id: '12345', verified: false} as User,
token: 'mockToken12345'
}
const mockAuthStore = {
model: {email: mockLoginUser.email, id: '12345', verified: false} as User,
token: 'abc123',
isValid: true,
clear: jest.fn(),
};
const mockCollection = {
authWithPassword: jest.fn().mockResolvedValue({
record: { verified: true }
}),
create: jest.fn(),
requestPasswordReset: jest.fn(),
requestVerification: jest.fn().mockResolvedValue(true),
};
const mockPocketBase = jest.fn(() => ({
collection: jest.fn(() => mockCollection),
authStore: mockAuthStore,
}));
beforeEach(() => {
TestBed.configureTestingModule({
providers: [],
imports: []
});
authService = TestBed.inject(AuthService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be created', () => {
expect(authService).toBeTruthy();
});
it('should have user signal initialized to undefined', () => {
expect(authService.user()).toBeUndefined();
});
it('should login correctly and set signal', async () => {
authService.login(mockLoginUser)
.then((response) => {
expect(response).toBeDefined();
}).catch((error) => jest.fn());
});
it('should make success register', () => {
authService.register(mockRegisterUser)
.then((response) => {
expect(response).toBeDefined();
expect(response.email).toEqual(mockRegisterUser.email);
}).catch((error) => jest.fn());
});
});

View File

@@ -0,0 +1,71 @@
import {Injectable, signal} from '@angular/core';
import {LoginDto} from "@app/shared/models/login-dto";
import PocketBase from 'pocketbase';
import {environment} from "@env/environment";
import {Auth} from "@app/shared/models/auth";
import {RegisterDto} from "@app/shared/models/register-dto";
import {User} from "@app/shared/models/user";
@Injectable({
providedIn: 'root'
})
export class AuthService {
user = signal<Auth | undefined>(undefined)
async login(loginDto: LoginDto) {
const {email, password} = loginDto
const pb = new PocketBase(environment.baseUrl);
const response = await pb.collection('users').authWithPassword(email, password);
const isValid = response.record['verified'];
const res = {isValid, record: pb.authStore.model as User, token: pb.authStore.token};
if (isValid){
this.user.set(res);
}
return res;
}
async register(registerDto: RegisterDto) {
const pb = new PocketBase(environment.baseUrl);
return await pb.collection('users').create<User>(registerDto)
}
async logout() {
const pb = new PocketBase(environment.baseUrl);
return pb.authStore.clear();
}
updateUser() {
const pb = new PocketBase(environment.baseUrl);
this.user.set({isValid: pb.authStore.isValid, record: pb.authStore.model as User, token: pb.authStore.token});
}
sendPasswordReset(email: string) {
const pb = new PocketBase(environment.baseUrl);
return pb.collection('users').requestPasswordReset(email);
}
verifyEmail(email: string) {
const pb = new PocketBase(environment.baseUrl);
return pb.collection('users').requestVerification(email)
.then(() => {
return true;
})
.catch((error) => {
console.error('Error sending verification email:', error);
return false;
});
}
isAuthenticated(): boolean {
const pb = new PocketBase(environment.baseUrl);
return pb.authStore.isValid;
}
isEmailVerified(): boolean {
const pb = new PocketBase(environment.baseUrl);
return pb.authStore.model? pb.authStore.model['verified'] : false;
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ProfileService } from './profile.service';
describe('ProfileService', () => {
let service: ProfileService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ProfileService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,40 @@
import {Injectable} from '@angular/core';
import PocketBase from "pocketbase";
import {environment} from "@env/environment";
import {Profile} from "@app/shared/models/profile";
import {from} from "rxjs";
import {ProfileDto} from "@app/shared/models/profile-dto";
@Injectable({
providedIn: 'root'
})
export class ProfileService {
createProfile(profileDto: ProfileDto) {
const pb = new PocketBase(environment.baseUrl);
return from(
pb.collection('profiles').create(profileDto)
);
}
get profiles() {
const pb = new PocketBase(environment.baseUrl);
return from(
pb.collection('profiles').getFullList<Profile>({
sort: 'profession'
}))
}
getProfileByUserId(userId: string) {
const pb = new PocketBase(environment.baseUrl);
return from(
pb.collection<Profile>('profiles').getFirstListItem(`utilisateur="${userId}"`)
)
}
updateProfile(id: string, data: Profile | any) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection('profiles').update<Profile>(id, data));
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ProjectService } from './project.service';
describe('ProjectService', () => {
let service: ProjectService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ProjectService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,34 @@
import {Injectable} from '@angular/core';
import PocketBase from "pocketbase";
import {environment} from "@env/environment.development";
import {from} from "rxjs";
import {Project} from "@app/shared/models/project";
import {ProjectDto} from "@app/shared/models/project-dto";
@Injectable({
providedIn: 'root'
})
export class ProjectService {
createProject(projectDto: ProjectDto) {
const pb = new PocketBase(environment.baseUrl);
return from(
pb.collection('projets').create<Project>(projectDto)
);
}
getProjectByUserId(userId: string) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection<Project>('projets').getFullList({filter: `utilisateur='${userId}'`}))
}
getProjectById(id: string) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection<Project>('projets').getOne<Project>(id))
}
updateProject(id: string, data: Project | any) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection('projets').update<Project>(id, data));
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SectorService } from './sector.service';
describe('SectorService', () => {
let service: SectorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SectorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,26 @@
import {Injectable} from '@angular/core';
import PocketBase from "pocketbase";
import {environment} from "@env/environment.development";
import {from} from "rxjs";
import {Sector} from "@app/shared/models/sector";
@Injectable({
providedIn: 'root'
})
export class SectorService {
get sectors() {
const pb = new PocketBase(environment.baseUrl);
return from(
pb.collection<Sector>('secteur').getFullList({
sort: 'nom'
}))
}
getSectorById(id: string) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection<Sector>('secteur').getOne<Sector>(id))
}
}

View File

@@ -1,16 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ThemeService } from './theme.service';
describe('ThemeService', () => {
let service: ThemeService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ThemeService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
import { TestBed } from '@angular/core/testing';
import { ThemeService } from './theme.service';
describe('ThemeService', () => {
let service: ThemeService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ThemeService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,14 +1,14 @@
import {Injectable, signal} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
darkModeSignal = signal<string>('null');
updateDarkMode() {
this.darkModeSignal.update((value) => (value === 'dark' ? 'null' : 'dark'));
}
}
import {Injectable, signal} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
darkModeSignal = signal<string>('null');
updateDarkMode() {
this.darkModeSignal.update((value) => (value === 'dark' ? 'null' : 'dark'));
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UserService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import {Injectable} from '@angular/core';
import PocketBase from "pocketbase";
import {environment} from "@env/environment";
import {from} from "rxjs";
import {User} from "@app/shared/models/user";
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserById(id: string) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection('users').getOne<User>(id));
}
updateUser(id: string, data: User|any) {
const pb = new PocketBase(environment.baseUrl);
return from(pb.collection('users').update<User>(id, data));
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthComponent } from './auth.component';
describe('AuthComponent', () => {
let component: AuthComponent;
let fixture: ComponentFixture<AuthComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AuthComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AuthComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthComponent } from './auth.component';
import {provideRouter} from "@angular/router";
describe('AuthComponent', () => {
let component: AuthComponent;
let fixture: ComponentFixture<AuthComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AuthComponent],
providers:[
provideRouter([])
]
})
.compileComponents();
fixture = TestBed.createComponent(AuthComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,16 +1,16 @@
import { Component } from '@angular/core';
import {RouterLink, RouterOutlet} from "@angular/router";
@Component({
selector: 'app-auth',
standalone: true,
imports: [
RouterOutlet,
RouterLink
],
templateUrl: './auth.component.html',
styleUrl: './auth.component.scss'
})
export class AuthComponent {
}
import { Component } from '@angular/core';
import {RouterLink, RouterOutlet} from "@angular/router";
@Component({
selector: 'app-auth',
standalone: true,
imports: [
RouterOutlet,
RouterLink
],
templateUrl: './auth.component.html',
styleUrl: './auth.component.scss'
})
export class AuthComponent {
}

View File

@@ -1,24 +1,24 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {RegisterComponent} from "@app/shared/features/register/register.component";
import {AuthComponent} from "@app/routes/authentification/auth/auth.component";
import {LoginComponent} from "@app/shared/features/login/login.component";
const routes: Routes = [
{
path: '', component: AuthComponent, children: [
{path: '', component: LoginComponent},
{path: 'register', component: RegisterComponent},
]
},
{path: 'not-found', loadChildren: () => import('@app/routes/not-found/not-found.module').then(m => m.NotFoundModule)},
{path: '**', redirectTo: 'not-found'}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthentificationRoutingModule {
}
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {RegisterComponent} from "@app/shared/features/register/register.component";
import {AuthComponent} from "@app/routes/authentification/auth/auth.component";
import {LoginComponent} from "@app/shared/features/login/login.component";
const routes: Routes = [
{
path: '', component: AuthComponent, children: [
{path: '', component: LoginComponent},
{path: 'register', component: RegisterComponent},
]
},
{path: 'not-found', loadChildren: () => import('@app/routes/not-found/not-found.module').then(m => m.NotFoundModule)},
{path: '**', redirectTo: 'not-found'}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthentificationRoutingModule {
}

View File

@@ -1,14 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthentificationRoutingModule } from './authentification-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
AuthentificationRoutingModule
]
})
export class AuthentificationModule { }
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AuthentificationRoutingModule} from './authentification-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
AuthentificationRoutingModule
]
})
export class AuthentificationModule { }

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {HomeComponent} from "@app/routes/home/home.component";
const routes: Routes = [
{path: '', component: HomeComponent, title: 'Accueil'}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {HomeComponent} from "@app/routes/home/home.component";
const routes: Routes = [
{path: '', component: HomeComponent, title: 'Accueil'}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }

View File

@@ -1,70 +1,28 @@
<section class="pb-10 relative">
<div className="absolute inset-0 bg-heroPatternLight dark:bg-heroPatternDark"></div>
<div class="relative overflow-hidden">
<div class="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8 pt-24 pb-10">
<div class="mt-5 max-w-2xl text-center mx-auto">
<h1
class="block font-bold text-gray-800 dark:text-white text-3xl md:text-5xl lg:text-6xl">
Dans quel secteur se cache votre prochaine
<span class="text-gray-800 dark:text-white">pépites?</span>
<div id=container>
<div id=flip>
<div><div>Les finances</div></div>
<div><div>La Santé</div></div>
<div><div>Les Etudes</div></div>
</div>
</div>
</h1>
</div>
<div class="mt-8 mx-auto max-w-3xl space-y-2">
<app-search (onSearchChange)="showNewQuery($event)" />
</div>
</div>
</div>
<div class="max-w-6xl mx-auto px-4">
<div
class="w-full relative bg-purple-200 rounded-xl px-2 pt-8 space-y-4 md:space-y-0">
<div
class="relative md:absolute inset-0 z-[1] bg-transparent md:px-10 order-0">
<div class="h-full grid place-content-center md:w-1/2 lg:w-2/5">
<div class="px-8 space-y-3 text-center md:text-left">
<h1
class="text-purple-950 text-4xl font-bold mb-4 text-center md:text-left">
Votre prochain profile en quelques clicks
</h1>
<p class="text-center md:text-left">
Le moyen le plus simple de trouver le/la candidat(e) qu'il vous faut.
</p>
<button
class="items-center flex gap-2 px-12 py-3 rounded-full text-white bg-purple-900 mx-auto md:m-0">
<span class="inline-block w-5 h-5">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-full h-full fill-current"
viewBox="0 -960 960 960">
<path
d="M480-80Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552l84-84 56 56-180 180-180-180 56-56 84 84q0-109-69.5-178.5T480-800q-101 0-170.5 69.5T240-552q0 71 59 162.5T480-186q20-18 37-35l34-34q-5-10-8-21.5t-3-23.5q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29q-8 0-14.5-1t-13.5-3q-29 30-61.5 61T480-80Z" />
</svg>
</span>
<span>Réchercher maintenant</span>
</button>
</div>
</div>
</div>
<img
class="w-full h-auto hidden md:inline-block"
src="https://www.tripadvisor.ca/img2/trips/home-gai-entry-dv.png"
alt="" />
<img
class="w-full h-auto block md:hidden"
src="https://www.tripadvisor.ca/img2/trips/home-gai-entry-mv.png"
alt="" />
</div>
</div>
</section>
<section class="">
<div class="w-full mx-auto px-4 sm:px-6 lg:px-8 pt-24 pb-10">
<div class="mt-5 max-w-2xl text-center mx-auto">
<h1
class="block font-bold text-gray-800 gap-6 dark:text-white text-3xl md:text-5xl lg:text-6xl">
Dans quel secteur se cache votre prochaine
<br>
<span class="text-gray-800 dark:text-white">pépites?</span>
<div class="word-animation max-sm:h-10 max-md:h-11 h-16">
<span class="red">Les finances</span>
<span class="orange">La Santé</span>
<span class="blue">Les Etudes</span>
</div>
</h1>
</div>
<div class="mt-8 mx-auto w-full space-y-2">
<app-search (onSearchChange)="showNewQuery($event)"/>
</div>
</div>
</section>

View File

@@ -1,46 +1,67 @@
#container {
color:#999;
text-transform: uppercase;
font-size:36px;
font-weight:bold;
display:block;
}
#flip {
margin-top: 10px;
height:50px;
overflow:hidden;
}
#flip > div > div {
color:#fff;
padding:4px 12px;
height:45px;
margin-bottom:45px;
display:inline-block;
}
#flip div:first-child {
animation: show 10s linear infinite;
}
#flip div div {
background:#42c58a;
}
#flip div:first-child div {
background:#4ec7f3;
}
#flip div:last-child div {
background:#DC143C;
}
@keyframes show {
0% {margin-top:-270px;}
5% {margin-top:-180px;}
33% {margin-top:-180px;}
38% {margin-top:-90px;}
66% {margin-top:-90px;}
71% {margin-top:0px;}
99.99% {margin-top:0px;}
100% {margin-top:-270px;}
}
h1 {
.word-animation {
overflow: hidden;
span {
color: #4ec7f3;
display: block;
text-transform: capitalize;
animation: rotateSpin 10s infinite;
&.red {
color: red;
}
&.orange {
color: orange;
}
&.blue {
color: blue;
}
}
}
}
@keyframes rotateSpin {
10% {
-webkit-transform-style: translateY(-102%);
transform: translateY(-102%);
}
25% {
-webkit-transform-style: translateY(-100%);
transform: translateY(-100%);
}
35% {
-webkit-transform-style: translateY(-202%);
transform: translateY(-202%);
}
50% {
-webkit-transform-style: translateY(-200%);
transform: translateY(-200%);
}
60% {
-webkit-transform-style: translateY(-302%);
transform: translateY(-302%);
}
75% {
-webkit-transform-style: translateY(-300%);
transform: translateY(-300%);
}
85% {
-webkit-transform-style: translateY(-402%);
transform: translateY(-402%);
}
100% {
-webkit-transform-style: translateY(-400%);
transform: translateY(-400%);
}
}

View File

@@ -1,23 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,23 +1,23 @@
import {Component, inject} from '@angular/core';
import {FaIconComponent} from "@fortawesome/angular-fontawesome";
import {SearchComponent} from "@app/shared/features/search/search.component";
import {Router} from "@angular/router";
@Component({
selector: 'app-home',
standalone: true,
imports: [
FaIconComponent,
SearchComponent
],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
})
export class HomeComponent {
private readonly router = inject(Router)
showNewQuery(newQuery: string) {
this.router.navigate(['/profiles'], {queryParams: {search: newQuery}});
}
}
import {Component, inject} from '@angular/core';
import {FaIconComponent} from "@fortawesome/angular-fontawesome";
import {SearchComponent} from "@app/shared/features/search/search.component";
import {Router} from "@angular/router";
@Component({
selector: 'app-home',
standalone: true,
imports: [
FaIconComponent,
SearchComponent
],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
})
export class HomeComponent {
private readonly router = inject(Router)
showNewQuery(newQuery: string) {
this.router.navigate(['/profiles'], {queryParams: {search: newQuery}});
}
}

View File

@@ -1,14 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeRoutingModule } from './home-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
HomeRoutingModule
]
})
export class HomeModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeRoutingModule } from './home-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
HomeRoutingModule
]
})
export class HomeModule { }

View File

@@ -1,14 +1,17 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {MyProfileComponent} from "@app/routes/my-profile/my-profile.component";
const routes: Routes = [
{path: '', component: MyProfileComponent}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MyProfileRoutingModule {
}
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {MyProfileComponent} from "@app/routes/my-profile/my-profile.component";
import {myProfileResolver} from "@app/core/resolvers/my-profile/my-profile.resolver";
const routes: Routes = [
{
path: '', component: MyProfileComponent, resolve: {user: myProfileResolver}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MyProfileRoutingModule {
}

View File

@@ -1 +1,192 @@
<p>my-profile works!</p>
@if (profile != undefined) {
<section class="text-gray-600">
<div class="container px-5 py-5 mx-auto flex flex-col">
<div class="w-full max-md:mx-5 mx-auto my-5 rounded-lg min-h-56 max-h-64 bg-cover bg-auth">
<div class="w-max flex justify-between items-center">
<a [routerLink]="['']" (click)="location.back()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="hover:text-gray-300 text-white size-6 w-5 h-5 sm:w-9 sm:h-9 mx-4 my-4 hover:w-10 hover:h-10 ">
<title>Retour</title>
<path fill-rule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-4.28 9.22a.75.75 0 0 0 0 1.06l3 3a.75.75 0 1 0 1.06-1.06l-1.72-1.72h5.69a.75.75 0 0 0 0-1.5h-5.69l1.72-1.72a.75.75 0 0 0-1.06-1.06l-3 3Z"
clip-rule="evenodd"/>
</svg>
</a>
@if (profile.estVerifier) {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class=" hover:text-purple-300 size-6 text-purple-500 w-6 h-6 sm:w-11 sm:h-11 text-end mx-4 my-4">
<title>Profile verifier</title>
<path fill-rule="evenodd"
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clip-rule="evenodd"/>
</svg>
}
</div>
</div>
<div class="flex flex-col justify-center sm:flex-row sm:justify-between">
<div class=" text-center sm:pr-8 sm:py-8">
@if (!isEditMode()) {
<div class="w-max" (click)="isEditMode.set(!isEditMode())">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="w-6 h-6 cursor-pointer hover:text-gray-800 dark:text-white">
<title>Editer le profil</title>
<path
d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32l8.4-8.4Z"/>
<path
d="M5.25 5.25a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3V13.5a.75.75 0 0 0-1.5 0v5.25a1.5 1.5 0 0 1-1.5 1.5H5.25a1.5 1.5 0 0 1-1.5-1.5V8.25a1.5 1.5 0 0 1 1.5-1.5h5.25a.75.75 0 0 0 0-1.5H5.25Z"/>
</svg>
</div>
}
@if (!isEditMode()) {
<div class="w-28 h-28 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400">
@if (user().avatar) {
<img alt="{{user().username}}" class="object-cover object-center h-full w-full rounded-full"
src="{{environment.baseUrl}}/api/files/users/{{user().id}}/{{user().avatar}}">
} @else {
<img alt="{{user().username}}" class="object-cover object-center h-full w-full rounded-full"
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{user().username}}">
}
</div>
<div class="flex flex-col w-full items-center text-center justify-center">
@if (user().name) {
<h2
class="font-medium title-font mt-4 text-gray-900 text-lg w-full dark:text-white">{{ user().name }}</h2>
} @else if (user().username) {
<h2
class="font-medium title-font mt-4 text-gray-900 text-lg w-full dark:text-white">{{ user().username }}</h2>
} @else {
<h2
class="font-medium title-font mt-4 text-gray-900 text-lg w-full dark:text-white">{{ user().email }}</h2>
}
<div class="w-12 h-1 bg-indigo-500 rounded mt-2 mb-4"></div>
@if (profile.bio) {
<p class="text-base dark:text-white w-full">{{ profile.bio }}</p>
} @else {
<p class="text-base dark:text-white w-full">Je suis sur la plateforme Trouve Ton Profile pour partager
mon
expertise et mes
compétences. Nhésitez pas à me contacter pour en savoir plus sur mon parcours et mes domaines
dintervention.</p>
}
@if (profile.secteur) {
<div class="space-y-2 flex flex-col my-4">
<p class="text-base dark:text-white">Secteur</p>
<app-chips [sectorId]="profile.secteur"/>
</div>
}
@if (profile.reseaux) {
<div class="space-y-2 flex flex-col my-4">
<p class="text-base dark:text-white">Réseaux</p>
<app-reseaux [reseaux]="profile.reseaux"/>
</div>
}
</div>
<a [href]="qrCodeDownloadLink" download="qrcode"
class="w-full flex items-center justify-center sm:pr-8 sm:py-8">
<qrcode (qrCodeURL)="onChangeURL($event)" [qrdata]="myProfileQrCode" [width]="128" [elementType]="'url'"
[errorCorrectionLevel]="'M'" class="mx-auto"></qrcode>
</a>
}
@if (isEditMode()) {
<app-update-user [user]="user()" (isCancelEditMode)="onCancelEditMode($event)"/>
}
</div>
<div
class="flex-1 sm:pl-8 sm:py-8 sm:border-l border-gray-200 sm:border-t-0 border-t mt-4 pt-4 sm:mt-0 text-center sm:text-left">
<div class=" p-4">
<ul class="w-full flex flex-wrap gap-x-2 gap-y-2 rounded-lg items-center max-sm:mx-auto">
<li id="homeTab" (click)="menu.set('home')"
[ngClass]="{'border-blue-600 text-blue-600': menu()=='home'.toLowerCase()}"
class="tab flex flex-col justify-center items-center border-2 hover:border-blue-600 rounded-lg bg-gray-100 text-sm font-semibold hover:text-blue-600 py-2 px-2 min-w-[100px] cursor-pointer transition-all">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-5 mb-1.5"
viewBox="0 0 511 511.999">
<path
d="M498.7 222.695c-.016-.011-.028-.027-.04-.039L289.805 13.81C280.902 4.902 269.066 0 256.477 0c-12.59 0-24.426 4.902-33.332 13.809L14.398 222.55c-.07.07-.144.144-.21.215-18.282 18.386-18.25 48.218.09 66.558 8.378 8.383 19.44 13.235 31.273 13.746.484.047.969.07 1.457.07h8.32v153.696c0 30.418 24.75 55.164 55.168 55.164h81.711c8.285 0 15-6.719 15-15V376.5c0-13.879 11.293-25.168 25.172-25.168h48.195c13.88 0 25.168 11.29 25.168 25.168V497c0 8.281 6.715 15 15 15h81.711c30.422 0 55.168-24.746 55.168-55.164V303.14h7.719c12.586 0 24.422-4.903 33.332-13.813 18.36-18.367 18.367-48.254.027-66.633zm-21.243 45.422a17.03 17.03 0 0 1-12.117 5.024h-22.72c-8.285 0-15 6.714-15 15v168.695c0 13.875-11.289 25.164-25.168 25.164h-66.71V376.5c0-30.418-24.747-55.168-55.169-55.168H232.38c-30.422 0-55.172 24.75-55.172 55.168V482h-66.71c-13.876 0-25.169-11.29-25.169-25.164V288.14c0-8.286-6.715-15-15-15H48a13.9 13.9 0 0 0-.703-.032c-4.469-.078-8.66-1.851-11.8-4.996-6.68-6.68-6.68-17.55 0-24.234.003 0 .003-.004.007-.008l.012-.012L244.363 35.02A17.003 17.003 0 0 1 256.477 30c4.574 0 8.875 1.781 12.113 5.02l208.8 208.796.098.094c6.645 6.692 6.633 17.54-.031 24.207zm0 0"
data-original="#000000"></path>
</svg>
Mon profile
</li>
<li id="settingTab" (click)="menu.set('projects')"
[ngClass]="{'border-blue-600 text-blue-600': menu()=='projects'.toLowerCase()}"
class="tab flex flex-col justify-center items-center border-2 hover:border-blue-600 rounded-lg bg-gray-100 text-sm font-semibold hover:text-blue-600 py-2 px-2 min-w-[100px] cursor-pointer transition-all">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.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 0 1 12 15a9.065 9.065 0 0 0-6.23-.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"/>
</svg>
Mes projets
</li>
<li id="profileTab" (click)="menu.set('update')"
[ngClass]="{'border-blue-600 text-blue-600': menu()=='update'.toLowerCase()}"
class="tab flex flex-col justify-center items-center border-2 hover:border-blue-600 rounded-lg bg-gray-100 text-sm font-semibold hover:text-blue-600 py-2 px-2 min-w-[100px] cursor-pointer transition-all">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-5 mb-1.5" viewBox="0 0 512 512">
<path
d="M437.02 74.98C388.668 26.63 324.379 0 256 0S123.332 26.629 74.98 74.98C26.63 123.332 0 187.621 0 256s26.629 132.668 74.98 181.02C123.332 485.37 187.621 512 256 512s132.668-26.629 181.02-74.98C485.37 388.668 512 324.379 512 256s-26.629-132.668-74.98-181.02zM111.105 429.297c8.454-72.735 70.989-128.89 144.895-128.89 38.96 0 75.598 15.179 103.156 42.734 23.281 23.285 37.965 53.687 41.742 86.152C361.641 462.172 311.094 482 256 482s-105.637-19.824-144.895-52.703zM256 269.507c-42.871 0-77.754-34.882-77.754-77.753C178.246 148.879 213.13 114 256 114s77.754 34.879 77.754 77.754c0 42.871-34.883 77.754-77.754 77.754zm170.719 134.427a175.9 175.9 0 0 0-46.352-82.004c-18.437-18.438-40.25-32.27-64.039-40.938 28.598-19.394 47.426-52.16 47.426-89.238C363.754 132.34 315.414 84 256 84s-107.754 48.34-107.754 107.754c0 37.098 18.844 69.875 47.465 89.266-21.887 7.976-42.14 20.308-59.566 36.542-25.235 23.5-42.758 53.465-50.883 86.348C50.852 364.242 30 312.512 30 256 30 131.383 131.383 30 256 30s226 101.383 226 226c0 56.523-20.86 108.266-55.281 147.934zm0 0"
data-original="#000000"></path>
</svg>
Mes informations
</li>
<li id="cvTab" (click)="menu.set('cv')"
[ngClass]="{'border-blue-600 text-blue-600': menu()=='cv'.toLowerCase()}"
class="tab flex flex-col justify-center items-center border-2 hover:border-blue-600 rounded-lg bg-gray-100 text-sm font-semibold hover:text-blue-600 py-2 px-2 min-w-[100px] cursor-pointer transition-all">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/>
</svg>
Lecteur de PDF
</li>
</ul>
<div id="homeContent" class="tab-content max-w-2xl block mt-8">
@switch (menu().toLowerCase()) {
@case ('home'.toLowerCase()) {
<app-my-home-profile [profile]="profile"/>
}
@case ('projects'.toLowerCase()) {
<app-my-profile-project-list [projectIds]="profile.projets" [userId]="user().id"/>
<router-outlet/>
}
@case ('update'.toLowerCase()) {
<app-my-profile-update-form [profile]="profile"/>
}
@case ('cv'.toLowerCase()) {
<app-pdf-viewer [profile]="profile"/>
}
@default {
<app-my-home-profile [profile]="profile"/>
}
}
</div>
</div>
</div>
</div>
</div>
</section>
}

View File

@@ -1,23 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileComponent } from './my-profile.component';
describe('MyProfileComponent', () => {
let component: MyProfileComponent;
let fixture: ComponentFixture<MyProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyProfileComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MyProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileComponent } from './my-profile.component';
import {provideRouter} from "@angular/router";
describe('MyProfileComponent', () => {
let component: MyProfileComponent;
let fixture: ComponentFixture<MyProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyProfileComponent],
providers: [
provideRouter([])
]
})
.compileComponents();
fixture = TestBed.createComponent(MyProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,12 +1,87 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-my-profile',
standalone: true,
imports: [],
templateUrl: './my-profile.component.html',
styleUrl: './my-profile.component.scss'
})
export class MyProfileComponent {
}
import {Component, computed, inject, OnInit, signal} from '@angular/core';
import {ActivatedRoute, RouterLink, RouterOutlet} from "@angular/router";
import {User} from "@app/shared/models/user";
import {AsyncPipe, JsonPipe, Location, NgClass, UpperCasePipe} from "@angular/common";
import {UntilDestroy} from "@ngneat/until-destroy";
import {SafeUrl} from "@angular/platform-browser";
import {QRCodeModule} from "angularx-qrcode";
import {environment} from "@env/environment";
import {ChipsComponent} from "@app/shared/components/chips/chips.component";
import {ReseauxComponent} from "@app/shared/components/reseaux/reseaux.component";
import {UpdateUserComponent} from "@app/shared/features/update-user/update-user.component";
import {
MyProfileProjectListComponent
} from "@app/shared/components/my-profile-project-list/my-profile-project-list.component";
import {MyHomeProfileComponent} from "@app/shared/components/my-home-profile/my-home-profile.component";
import {
MyProfileUpdateFormComponent
} from "@app/shared/components/my-profile-update-form/my-profile-update-form.component";
import {ProfileService} from "@app/core/services/profile/profile.service";
import {Profile} from "@app/shared/models/profile";
import {PdfViewerComponent} from "@app/shared/features/pdf-viewer/pdf-viewer.component";
@Component({
selector: 'app-my-profile',
standalone: true,
imports: [
JsonPipe,
RouterLink,
AsyncPipe,
QRCodeModule,
ChipsComponent,
ReseauxComponent,
UpdateUserComponent,
UpperCasePipe,
MyProfileProjectListComponent,
RouterOutlet,
MyHomeProfileComponent,
MyProfileUpdateFormComponent,
NgClass,
PdfViewerComponent
],
templateUrl: './my-profile.component.html',
styleUrl: './my-profile.component.scss'
})
@UntilDestroy()
export class MyProfileComponent implements OnInit {
private profileService = inject(ProfileService);
protected readonly environment = environment;
protected menu = signal<string>("home");
protected myProfileQrCode: string = `${environment.production}`;
protected qrCodeDownloadLink: SafeUrl = `${environment.production}`;
protected location = inject(Location);
protected readonly route = inject(ActivatedRoute);
protected extraData: { user: User } = this.route.snapshot.data['user'];
protected user = computed(() => {
if (this.extraData != undefined) return this.extraData.user;
return {} as User;
});
protected profile: Profile = {} as Profile;
protected isEditMode = signal<boolean>(false);
onChangeURL(url: SafeUrl) {
this.qrCodeDownloadLink = url;
}
onCancelEditMode($event: boolean) {
this.isEditMode.set(!$event);
}
ngOnInit(): void {
this.myProfileQrCode = `${this.myProfileQrCode}/profiles/${this.user().id}`;
this.profileService.getProfileByUserId(this.user().id).subscribe({
next: (value: Profile) => {
this.profile = value;
}
});
}
}

View File

@@ -1,14 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyProfileRoutingModule } from './my-profile-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
MyProfileRoutingModule
]
})
export class MyProfileModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyProfileRoutingModule } from './my-profile-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
MyProfileRoutingModule
]
})
export class MyProfileModule { }

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {NotFoundComponent} from "@app/routes/not-found/not-found.component";
const routes: Routes = [
{path: '', component: NotFoundComponent, title: 'Page non trouvée'}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class NotFoundRoutingModule { }
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {NotFoundComponent} from "@app/routes/not-found/not-found.component";
const routes: Routes = [
{path: '', component: NotFoundComponent, title: 'Page non trouvée'}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class NotFoundRoutingModule { }

View File

@@ -1 +1 @@
<p>not-found works!</p>
<p>not-found works!</p>

View File

@@ -1,23 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundComponent } from './not-found.component';
describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NotFoundComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NotFoundComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundComponent } from './not-found.component';
describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NotFoundComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NotFoundComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,12 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found',
standalone: true,
imports: [],
templateUrl: './not-found.component.html',
styleUrl: './not-found.component.scss'
})
export class NotFoundComponent {
}
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found',
standalone: true,
imports: [],
templateUrl: './not-found.component.html',
styleUrl: './not-found.component.scss'
})
export class NotFoundComponent {
}

View File

@@ -1,14 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NotFoundRoutingModule } from './not-found-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
NotFoundRoutingModule
]
})
export class NotFoundModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NotFoundRoutingModule } from './not-found-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
NotFoundRoutingModule
]
})
export class NotFoundModule { }

View File

@@ -1,35 +1,83 @@
<section class="text-gray-600 body-font">
<div class="container px-5 py-24 mx-auto flex flex-col">
<div class="lg:w-4/6 mx-auto">
<div class="rounded-lg h-64 overflow-hidden">
<img alt="content" class="object-cover object-center h-full w-full" src="https://dummyimage.com/1200x500">
</div>
<div class="flex flex-col sm:flex-row mt-10">
<div class="sm:w-1/3 text-center sm:pr-8 sm:py-8">
<div class="w-20 h-20 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400">
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-10 h-10" viewBox="0 0 24 24">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<div class="flex flex-col items-center text-center justify-center">
<h2 class="font-medium title-font mt-4 text-gray-900 text-lg">Phoebe Caulfield</h2>
<div class="w-12 h-1 bg-indigo-500 rounded mt-2 mb-4"></div>
<p class="text-base">Raclette knausgaard hella meggs normcore williamsburg enamel pin sartorial venmo tbh hot chicken gentrify portland.</p>
</div>
<a [href]="qrCodeDownloadLink" download="qrcode" class="w-full flex items-center justify-center sm:pr-8 sm:py-8">
<qrcode (qrCodeURL)="onChangeURL($event)" [qrdata]="myAngularxQrCode" [width]="128" [errorCorrectionLevel]="'M'" class="mx-auto"></qrcode>
</a>
</div>
<div class="sm:w-2/3 sm:pl-8 sm:py-8 sm:border-l border-gray-200 sm:border-t-0 border-t mt-4 pt-4 sm:mt-0 text-center sm:text-left">
<p class="leading-relaxed text-lg mb-4">Meggings portland fingerstache lyft, post-ironic fixie man bun banh mi umami everyday carry hexagon locavore direct trade art party. Locavore small batch listicle gastropub farm-to-table lumbersexual salvia messenger bag. Coloring book flannel truffaut craft beer drinking vinegar sartorial, disrupt fashion axe normcore meh butcher. Portland 90's scenester vexillologist forage post-ironic asymmetrical, chartreuse disrupt butcher paleo intelligentsia pabst before they sold out four loko. 3 wolf moon brooklyn.</p>
<a class="text-indigo-500 inline-flex items-center">Learn More
<svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="w-4 h-4 ml-2" viewBox="0 0 24 24">
<path d="M5 12h14M12 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
</div>
</div>
</section>
<section class="text-gray-600">
<div class="container px-5 py-12 mx-auto flex flex-col">
<div class="lg:w-4/6 mx-auto">
<div class="rounded-lg min-h-56 max-h-64 overflow-hidden bg-cover bg-auth">
<div class="w-full flex justify-between items-center">
<a [routerLink]="['/profiles']">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="hover:text-gray-300 text-white size-6 w-5 h-5 sm:w-9 sm:h-9 mx-4 my-4 hover:w-10 hover:h-10 ">
<title>Retour</title>
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-4.28 9.22a.75.75 0 0 0 0 1.06l3 3a.75.75 0 1 0 1.06-1.06l-1.72-1.72h5.69a.75.75 0 0 0 0-1.5h-5.69l1.72-1.72a.75.75 0 0 0-1.06-1.06l-3 3Z" clip-rule="evenodd" />
</svg>
</a>
@if (profile().estVerifier) {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class=" hover:text-purple-300 size-6 text-purple-500 w-6 h-6 sm:w-11 sm:h-11 text-end mx-4 my-4">
<title>Profile verifier</title>
<path fill-rule="evenodd"
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clip-rule="evenodd"/>
</svg>
}
</div>
<!-- <img alt="{{user().username}}" class="object-cover object-center h-full w-full" src="https://api.dicebear.com/9.x/adventurer/svg?seed={{user().username}}">-->
</div>
<div class="flex flex-col sm:flex-row mt-10">
<div class="sm:w-1/3 text-center sm:pr-8 sm:py-8">
<div class="w-20 h-20 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400">
@if (user().avatar) {
<img alt="{{user().username}}" class="object-cover object-center h-full w-full rounded-full"
src="{{environment.baseUrl}}/api/files/users/{{user().id}}/{{user().avatar}}" loading="lazy">
} @else {
<img alt="{{user().username}}" class="object-cover object-center h-full w-full rounded-full"
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{user().username}}" loading="lazy">
}
</div>
<div class="flex flex-col items-center text-center justify-center">
@if (user().name){
<h2 class="font-medium title-font mt-4 text-gray-900 text-lg dark:text-white">{{user().name}}</h2>
} @else if (user().username){
<h2 class="font-medium title-font mt-4 text-gray-900 text-lg dark:text-white">{{user().username}}</h2>
} @else {
<h2 class="font-medium title-font mt-4 text-gray-900 text-lg dark:text-white">{{user().email}}</h2>
}
<div class="w-12 h-1 bg-indigo-500 rounded mt-2 mb-4"></div>
@if (profile().bio) {
<p class="text-base dark:text-white">{{ profile().bio }}</p>
} @else {
<p class="text-base dark:text-white">Je suis sur la plateforme Trouve Ton Profile pour partager mon expertise et mes
compétences. Nhésitez pas à me contacter pour en savoir plus sur mon parcours et mes domaines
dintervention.</p>
}
@if(profile().secteur){
<div class="space-y-2 flex flex-col my-4">
<p class="text-base dark:text-white">Secteur</p>
<app-chips [sectorId]="profile().secteur"/>
</div>
}
@if (profile().reseaux){
<div class="space-y-2 flex flex-col my-4">
<p class="text-base dark:text-white">Réseaux</p>
<app-reseaux [reseaux]="profile().reseaux"/>
</div>
}
</div>
</div>
<div
class="sm:w-2/3 sm:pl-8 sm:py-8 sm:border-l border-gray-200 sm:border-t-0 border-t mt-4 pt-4 sm:mt-0 text-center sm:text-left flex-col flex space-y-2">
<h2 class="text-3xl font-extrabold text-black dark:text-white">{{profile().profession | uppercase}}</h2>
<p class="leading-relaxed text-lg mb-4 dark:text-white">{{profile().apropos}}</p>
<app-project-list [userProjectId]="profile().utilisateur"/>
</div>
</div>
</div>
</div>
</section>

View File

@@ -1,23 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileDetailComponent } from './profile-detail.component';
describe('ProfileDetailComponent', () => {
let component: ProfileDetailComponent;
let fixture: ComponentFixture<ProfileDetailComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileDetailComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProfileDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProfileDetailComponent} from './profile-detail.component';
import {provideRouter} from "@angular/router";
describe('ProfileDetailComponent', () => {
let component: ProfileDetailComponent;
let fixture: ComponentFixture<ProfileDetailComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileDetailComponent],
providers:[
provideRouter([])
]
})
.compileComponents();
fixture = TestBed.createComponent(ProfileDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,31 +1,46 @@
import {Component, inject} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {QRCodeModule} from "angularx-qrcode";
import {SafeUrl} from "@angular/platform-browser";
@Component({
selector: 'app-profile-detail',
standalone: true,
imports: [
QRCodeModule
],
templateUrl: './profile-detail.component.html',
styleUrl: './profile-detail.component.scss'
})
export class ProfileDetailComponent {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
protected profile = this.route.snapshot.data['profile'];
protected myAngularxQrCode: string = "http://loalhost:4200";
protected qrCodeDownloadLink: SafeUrl = "";
constructor() {
console.log(this.profile)
this.myAngularxQrCode = this.myAngularxQrCode + this.router.url;
}
onChangeURL(url: SafeUrl) {
this.qrCodeDownloadLink = url;
}
}
import {Component, computed, inject} from '@angular/core';
import {ActivatedRoute, RouterLink} from "@angular/router";
import {QRCodeModule} from "angularx-qrcode";
import {UpperCasePipe} from "@angular/common";
import {User} from "@app/shared/models/user";
import {Profile} from "@app/shared/models/profile";
import {ChipsComponent} from "@app/shared/components/chips/chips.component";
import {ReseauxComponent} from "@app/shared/components/reseaux/reseaux.component";
import {UntilDestroy} from "@ngneat/until-destroy";
import {ProjectListComponent} from "@app/shared/components/project-list/project-list.component";
import {environment} from "@env/environment";
@Component({
selector: 'app-profile-detail',
standalone: true,
imports: [
QRCodeModule,
ChipsComponent,
ReseauxComponent,
RouterLink,
UpperCasePipe,
ProjectListComponent
],
templateUrl: './profile-detail.component.html',
styleUrl: './profile-detail.component.scss'
})
@UntilDestroy()
export class ProfileDetailComponent {
protected readonly environment = environment;
private readonly route = inject(ActivatedRoute);
protected extraData: { user: User, profile: Profile } = this.route.snapshot.data['profile'];
protected user = computed(() => {
if (this.extraData != undefined) return this.extraData.user;
return {} as User;
});
protected profile = computed(() => {
if (this.extraData != undefined) return this.extraData.profile;
return {} as Profile;
});
}

View File

@@ -1,30 +1,28 @@
<section class="pb-10 relative">
<div class="relative overflow-hidden">
<div class="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8 pt-24 pb-10">
<div class="mt-8 mx-auto max-w-3xl space-y-2">
<app-search/>
</div>
</div>
</div>
<div class="max-w-6xl mx-auto px-4">
<app-display-profile-card (onDisplayChange)="showNewDisplay($event)"/>
@switch (display()){
@case ('list'.toUpperCase()){
<app-horizental-profile-list/>
}
@case ('grid'.toUpperCase()){
<app-vertical-profile-list/>
}
@default{
<app-vertical-profile-list/>
}
}
</div>
</section>
<section class="pb-10 relative">
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 pt-24 pb-10 flex sm:flex-row flex-col space-y-2 items-center sm:space-x-4 ">
<div class="flex-1">
<app-search/>
</div>
<div class="">
<app-display-profile-card (onDisplayChange)="showNewDisplay($event)"/>
</div>
</div>
<div class="max-w-6xl mx-auto px-4">
@switch (display()) {
@case ('list'.toUpperCase()) {
<app-horizental-profile-list [profiles]="profiles"/>
}
@case ('grid'.toUpperCase()) {
<app-vertical-profile-list [profiles]="profiles"/>
}
@default {
<app-vertical-profile-list [profiles]="profiles"/>
}
}
</div>
</section>

View File

@@ -1,23 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileListComponent } from './profile-list.component';
describe('ProfileListComponent', () => {
let component: ProfileListComponent;
let fixture: ComponentFixture<ProfileListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProfileListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileListComponent } from './profile-list.component';
import {provideRouter} from "@angular/router";
describe('ProfileListComponent', () => {
let component: ProfileListComponent;
let fixture: ComponentFixture<ProfileListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileListComponent],
providers:[
provideRouter([])
]
})
.compileComponents();
fixture = TestBed.createComponent(ProfileListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,47 +1,46 @@
import {Component, inject, signal} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {SearchComponent} from "@app/shared/features/search/search.component";
import {
HorizentalProfileItemComponent
} from "@app/shared/components/horizental-profile-item/horizental-profile-item.component";
import {
VerticalProfileItemComponent
} from "@app/shared/components/vertical-profile-item/vertical-profile-item.component";
import {DisplayProfileCardComponent} from "@app/shared/features/display-profile-card/display-profile-card.component";
import {JsonPipe} from "@angular/common";
import {
HorizentalProfileListComponent
} from "@app/shared/components/horizental-profile-list/horizental-profile-list.component";
import {
VerticalProfileListComponent
} from "@app/shared/components/vertical-profile-list/vertical-profile-list.component";
@Component({
selector: 'app-profile-list',
standalone: true,
imports: [
SearchComponent,
HorizentalProfileItemComponent,
VerticalProfileItemComponent,
DisplayProfileCardComponent,
JsonPipe,
HorizentalProfileListComponent,
VerticalProfileListComponent
],
templateUrl: './profile-list.component.html',
styleUrl: './profile-list.component.scss'
})
export class ProfileListComponent {
private readonly route = inject(ActivatedRoute);
protected profiles = this.route.snapshot.data['profiles'];
protected display = signal<string>('grid'.toUpperCase());
constructor() {
console.log(this.profiles)
}
showNewDisplay($event: string) {
this.display.set($event.toUpperCase())
}
}
import {Component, inject, signal} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {SearchComponent} from "@app/shared/features/search/search.component";
import {
HorizentalProfileItemComponent
} from "@app/shared/components/horizental-profile-item/horizental-profile-item.component";
import {
VerticalProfileItemComponent
} from "@app/shared/components/vertical-profile-item/vertical-profile-item.component";
import {DisplayProfileCardComponent} from "@app/shared/features/display-profile-card/display-profile-card.component";
import {JsonPipe} from "@angular/common";
import {
HorizentalProfileListComponent
} from "@app/shared/components/horizental-profile-list/horizental-profile-list.component";
import {
VerticalProfileListComponent
} from "@app/shared/components/vertical-profile-list/vertical-profile-list.component";
import {UntilDestroy} from "@ngneat/until-destroy";
import {Profile} from "@app/shared/models/profile";
@Component({
selector: 'app-profile-list',
standalone: true,
imports: [
SearchComponent,
HorizentalProfileItemComponent,
VerticalProfileItemComponent,
DisplayProfileCardComponent,
JsonPipe,
HorizentalProfileListComponent,
VerticalProfileListComponent
],
templateUrl: './profile-list.component.html',
styleUrl: './profile-list.component.scss'
})
@UntilDestroy()
export class ProfileListComponent {
private readonly route = inject(ActivatedRoute);
protected profiles : Profile[] = this.route.snapshot.data['profiles'] as Profile[];
protected display = signal<string>('grid'.toUpperCase());
showNewDisplay($event: string) {
this.display.set($event.toUpperCase())
}
}

View File

@@ -1,18 +1,18 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ProfileListComponent} from "@app/routes/profile/profile-list/profile-list.component";
import {ProfileDetailComponent} from "@app/routes/profile/profile-detail/profile-detail.component";
import {listResolver} from "@app/core/resolvers/profile/list/list.resolver";
import {detailResolver} from "@app/core/resolvers/profile/detail/detail.resolver";
const routes: Routes = [
{path: '', component: ProfileListComponent, title: 'Liste des profiles', resolve: {profiles: listResolver}},
{path: ':name', component: ProfileDetailComponent, title: 'Detail du profile', resolve: {profile: detailResolver}},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProfileRoutingModule {
}
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ProfileListComponent} from "@app/routes/profile/profile-list/profile-list.component";
import {ProfileDetailComponent} from "@app/routes/profile/profile-detail/profile-detail.component";
import {listResolver} from "@app/core/resolvers/profile/list/list.resolver";
import {detailResolver} from "@app/core/resolvers/profile/detail/detail.resolver";
const routes: Routes = [
{path: '', component: ProfileListComponent, title: 'Liste des profiles', resolve: {profiles: listResolver}},
{path: ':name', component: ProfileDetailComponent, title: 'Detail du profile', resolve: {profile: detailResolver}},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProfileRoutingModule {
}

View File

@@ -1,14 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProfileRoutingModule } from './profile-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
ProfileRoutingModule
]
})
export class ProfileModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProfileRoutingModule } from './profile-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
ProfileRoutingModule
]
})
export class ProfileModule { }

View File

@@ -0,0 +1,10 @@
@if (sector != undefined) {
<div class="flex flex-wrap space-x-2 items-center space-y-1">
@for (chip of sector.nom.split('-'); track chip) {
<small
class="rounded-full bg-indigo-400 hover:bg-indigo-700 text-xs text-white py-1.5 px-3 font-semibold ">{{ chip | titlecase }}</small>
}
</div>
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChipsComponent } from './chips.component';
describe('ChipsComponent', () => {
let component: ChipsComponent;
let fixture: ComponentFixture<ChipsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChipsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ChipsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,30 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import {Sector} from "@app/shared/models/sector";
import {TitleCasePipe} from "@angular/common";
import {SectorService} from "@app/core/services/sector/sector.service";
import {UntilDestroy} from "@ngneat/until-destroy";
@Component({
selector: 'app-chips',
standalone: true,
imports: [
TitleCasePipe
],
templateUrl: './chips.component.html',
styleUrl: './chips.component.scss'
})
@UntilDestroy()
export class ChipsComponent implements OnInit {
@Input({required: true}) sectorId: string | null = null;
protected sectorService = inject(SectorService);
protected sector: Sector | undefined = undefined;
ngOnInit(): void {
if (this.sectorId)
this.sectorService.getSectorById(this.sectorId).subscribe(value => this.sector = value)
}
}

View File

@@ -1,78 +1,78 @@
<footer class=" py-4 w-full min-h-full bg-white dark:bg-gray-900 ">
<div class="max-w-[80rem] mx-auto space-y-6 px-2 md:px-4 lg:px-6 pt-4">
<div class="h-px w-full bg-gray-900/10 bg-gray-800 dark:bg-white"></div>
<div
class="flex flex-row justify-between items-center text-sm text-gray-600">
<a
[routerLink]="['/']"
class="inline-flex items-center gap-1">
<span class="inline-block text-xl text-gray-800 dark:text-white">TrouveTonProfile</span>
</a>
<ul class="flex items-center gap-4">
<li>
<a [routerLink]="['/conditions']" class="inline-block text-gray-800 dark:text-white">Conditions</a>
</li>
<li>
<a [routerLink]="['/terms']" class="inline-block text-gray-800 dark:text-white">Terms</a>
</li>
<li>
<a [routerLink]="['/politiques']" class="inline-block text-gray-800 dark:text-white">Politiques</a>
</li>
</ul>
<ul class="flex items-center gap-4">
<li>
<a
target="_blank"
href="#"
class="inline-block rounded-full h-8 w-8 p-2 border text-gray-800 dark:text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-full h-full fill-current"
viewBox="0 0 448 512">
<path
d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z"/>
</svg>
</a>
</li>
<li>
<a
target="_blank"
href="#"
class="inline-block rounded-full h-8 w-8 p-2 border text-gray-800 dark:text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-full h-full fill-current"
viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2">
<path d="M0 0h24v24H0z"/>
<path
fill="currentColor"
d="M18 2a1 1 0 0 1 .993.883L19 3v4a1 1 0 0 1-.883.993L18 8h-3v1h3a1 1 0 0 1 .991 1.131l-.02.112l-1 4a1 1 0 0 1-.858.75L17 15h-2v6a1 1 0 0 1-.883.993L14 22h-4a1 1 0 0 1-.993-.883L9 21v-6H7a1 1 0 0 1-.993-.883L6 14v-4a1 1 0 0 1 .883-.993L7 9h2V8a6 6 0 0 1 5.775-5.996L15 2z"/>
</g>
</svg>
</a>
</li>
<li>
<a
target="_blank"
href="#"
class="inline-block rounded-full h-8 w-8 p-2 border text-gray-800 dark:text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-full h-full fill-current"
viewBox="0 0 512 512">
<path
d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/>
</svg>
</a>
</li>
</ul>
</div>
</div>
</footer>
<footer class=" py-4 w-full min-h-full bg-white dark:bg-gray-900 ">
<div class="max-w-[80rem] mx-auto space-y-6 px-2 md:px-4 lg:px-6 pt-4">
<div class="h-px w-full bg-gray-900/10 bg-gray-800 dark:bg-white"></div>
<div
class="flex flex-col sm:flex-row justify-between items-center text-sm text-gray-600 space-y-4 sm:space-y-0 ">
<a
[routerLink]="['/']"
class="inline-flex items-center gap-1">
<span class="inline-block text-xl text-gray-800 dark:text-white">TrouveTonProfile</span>
</a>
<ul class="flex items-center gap-4">
<li>
<a [routerLink]="['/conditions']" class="inline-block text-gray-800 dark:text-white">Conditions</a>
</li>
<li>
<a [routerLink]="['/terms']" class="inline-block text-gray-800 dark:text-white">Terms</a>
</li>
<li>
<a [routerLink]="['/politiques']" class="inline-block text-gray-800 dark:text-white">Politiques</a>
</li>
</ul>
<ul class="flex items-center gap-4">
<li>
<a
target="_blank"
href="#"
class="inline-block rounded-full h-8 w-8 p-2 border text-gray-800 dark:text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-full h-full fill-current"
viewBox="0 0 448 512">
<path
d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z"/>
</svg>
</a>
</li>
<li>
<a
target="_blank"
href="#"
class="inline-block rounded-full h-8 w-8 p-2 border text-gray-800 dark:text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-full h-full fill-current"
viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2">
<path d="M0 0h24v24H0z"/>
<path
fill="currentColor"
d="M18 2a1 1 0 0 1 .993.883L19 3v4a1 1 0 0 1-.883.993L18 8h-3v1h3a1 1 0 0 1 .991 1.131l-.02.112l-1 4a1 1 0 0 1-.858.75L17 15h-2v6a1 1 0 0 1-.883.993L14 22h-4a1 1 0 0 1-.993-.883L9 21v-6H7a1 1 0 0 1-.993-.883L6 14v-4a1 1 0 0 1 .883-.993L7 9h2V8a6 6 0 0 1 5.775-5.996L15 2z"/>
</g>
</svg>
</a>
</li>
<li>
<a
target="_blank"
href="#"
class="inline-block rounded-full h-8 w-8 p-2 border text-gray-800 dark:text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-full h-full fill-current"
viewBox="0 0 512 512">
<path
d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/>
</svg>
</a>
</li>
</ul>
</div>
</div>
</footer>

View File

@@ -1,23 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FooterComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FooterComponent],
providers:[
provideRouter([]),
]
})
.compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,15 +1,15 @@
import { Component } from '@angular/core';
import {RouterLink} from "@angular/router";
@Component({
selector: 'app-footer',
standalone: true,
imports: [
RouterLink
],
templateUrl: './footer.component.html',
styleUrl: './footer.component.scss'
})
export class FooterComponent {
}
import { Component } from '@angular/core';
import {RouterLink} from "@angular/router";
@Component({
selector: 'app-footer',
standalone: true,
imports: [
RouterLink
],
templateUrl: './footer.component.html',
styleUrl: './footer.component.scss'
})
export class FooterComponent {
}

View File

@@ -1,34 +1,43 @@
<div class="items-center bg-gray-50 rounded-lg shadow sm:flex dark:bg-gray-800 dark:border-gray-700 cursor-pointer" (click)="onShowDetail(user)">
<a href="#">
<img class="w-full rounded-lg sm:rounded-none sm:rounded-l-lg" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/avatars/bonnie-green.png" alt="Bonnie Avatar">
</a>
<div class="p-5">
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">
<a href="#">Bonnie Green</a>
</h3>
<span class="text-gray-500 dark:text-gray-400">CEO & Web Developer</span>
<p class="mt-3 mb-4 font-light text-gray-500 dark:text-gray-400">Bonnie drives the technical strategy of the flowbite platform and brand.</p>
<ul class="flex space-x-4 sm:mt-0">
<li>
<a href="#" class="text-gray-500 hover:text-gray-900 dark:hover:text-white">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clip-rule="evenodd" /></svg>
</a>
</li>
<li>
<a href="#" class="text-gray-500 hover:text-gray-900 dark:hover:text-white">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" /></svg>
</a>
</li>
<li>
<a href="#" class="text-gray-500 hover:text-gray-900 dark:hover:text-white">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" /></svg>
</a>
</li>
<li>
<a href="#" class="text-gray-500 hover:text-gray-900 dark:hover:text-white">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path fill-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2zm6.605 4.61a8.502 8.502 0 011.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.416 25.416 0 00-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362zM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.687 8.687 0 0112 3.475zm-3.633.803a53.896 53.896 0 013.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 014.729-5.975zM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.522 8.522 0 01-2.19-5.705zM12 20.547a8.482 8.482 0 01-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.318 35.318 0 011.823 6.475 8.4 8.4 0 01-3.341.684zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 01-3.655 5.715z" clip-rule="evenodd" /></svg>
</a>
</li>
</ul>
</div>
</div>
@if (user != undefined) {
<a [routerLink]="[user.username?user.username:user.id]" [state]="{user,profile}" class="cursor-pointer">
<div class="items-center bg-gray-50 rounded-lg shadow sm:flex dark:bg-gray-800 dark:border-gray-700 cursor-pointer">
<div class="sm:w-max w-full flex items-center justify-center ">
@if (user.avatar) {
<img class="max-w-xl rounded-lg max-h-64 object-cover sm:rounded-none sm:rounded-l-lg"
src="{{environment.baseUrl}}/api/files/users/{{user.id}}/{{user.avatar}}" alt="{{user.username}}"
loading="lazy">
} @else {
<img class="max-w-xl rounded-lg max-h-64 sm:rounded-none sm:rounded-l-lg"
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{user.username}}" alt="{{user.username}}"
loading="lazy">
}
</div>
<div class="p-5 flex flex-col items-center space-y-2">
@if (profile.estVerifier) {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6 text-purple-800">
<path fill-rule="evenodd"
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clip-rule="evenodd"/>
</svg>
}
@if (user.name) {
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">{{ user.name }}</h3>
} @else if (user.username) {
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">{{ user.username }}</h3>
} @else {
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">Non mentionné</h3>
}
<span class="text-gray-500 dark:text-gray-400">{{ profile.profession }}</span>
<app-chips [sectorId]="profile.secteur"/>
<app-reseaux [reseaux]="profile.reseaux"/>
</div>
</div>
</a>
}

View File

@@ -1,23 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HorizentalProfileItemComponent } from './horizental-profile-item.component';
describe('HorizentalProfileItemComponent', () => {
let component: HorizentalProfileItemComponent;
let fixture: ComponentFixture<HorizentalProfileItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HorizentalProfileItemComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HorizentalProfileItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HorizentalProfileItemComponent } from './horizental-profile-item.component';
describe('HorizentalProfileItemComponent', () => {
let component: HorizentalProfileItemComponent;
let fixture: ComponentFixture<HorizentalProfileItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HorizentalProfileItemComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HorizentalProfileItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,19 +1,37 @@
import {Component, inject, Input} from '@angular/core';
import {Router} from "@angular/router";
@Component({
selector: 'app-horizental-profile-item',
standalone: true,
imports: [],
templateUrl: './horizental-profile-item.component.html',
styleUrl: './horizental-profile-item.component.scss'
})
export class HorizentalProfileItemComponent {
@Input() user: any = {};
protected router = inject(Router)
onShowDetail(user: any) {
this.router.navigate(['/profiles', "user1"])
}
}
import {Component, inject, Input, OnInit} from '@angular/core';
import {Router, RouterLink} from "@angular/router";
import {Profile} from "@app/shared/models/profile";
import {UserService} from "@app/core/services/user/user.service";
import {User} from "@app/shared/models/user";
import {ChipsComponent} from "@app/shared/components/chips/chips.component";
import {ReseauxComponent} from "@app/shared/components/reseaux/reseaux.component";
import {environment} from "@env/environment";
@Component({
selector: 'app-horizental-profile-item',
standalone: true,
imports: [
RouterLink,
ChipsComponent,
ReseauxComponent
],
templateUrl: './horizental-profile-item.component.html',
styleUrl: './horizental-profile-item.component.scss'
})
export class HorizentalProfileItemComponent implements OnInit {
@Input({required: true}) profile: Profile = {} as Profile;
protected router = inject(Router)
protected userService = inject(UserService);
protected user: User | undefined = undefined;
ngOnInit(): void {
this.userService.getUserById(this.profile.utilisateur).subscribe(
value => this.user = value
)
}
protected readonly environment = environment;
}

View File

@@ -1,11 +1,13 @@
<section class="bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 lg:px-6 ">
<div class="grid gap-8 mb-6 lg:mb-16 md:grid-cols-2">
@for (n of [1, 2, 3, 4]; track n) {
<app-horizental-profile-item/>
}
</div>
</div>
</section>
<section class="bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 lg:px-6 ">
<div class="grid gap-8 mb-6 lg:mb-16 md:grid-cols-2">
@for (profile of profiles; track profile.id) {
<app-horizental-profile-item [profile]="profile"/>
} @empty {
<p>Aucun profile trouvée</p>
}
</div>
</div>
</section>

View File

@@ -1,23 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HorizentalProfileListComponent } from './horizental-profile-list.component';
describe('HorizentalProfileListComponent', () => {
let component: HorizentalProfileListComponent;
let fixture: ComponentFixture<HorizentalProfileListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HorizentalProfileListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HorizentalProfileListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HorizentalProfileListComponent } from './horizental-profile-list.component';
describe('HorizentalProfileListComponent', () => {
let component: HorizentalProfileListComponent;
let fixture: ComponentFixture<HorizentalProfileListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HorizentalProfileListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HorizentalProfileListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,17 +1,18 @@
import { Component } from '@angular/core';
import {
HorizentalProfileItemComponent
} from "@app/shared/components/horizental-profile-item/horizental-profile-item.component";
@Component({
selector: 'app-horizental-profile-list',
standalone: true,
imports: [
HorizentalProfileItemComponent
],
templateUrl: './horizental-profile-list.component.html',
styleUrl: './horizental-profile-list.component.scss'
})
export class HorizentalProfileListComponent {
}
import {Component, Input} from '@angular/core';
import {
HorizentalProfileItemComponent
} from "@app/shared/components/horizental-profile-item/horizental-profile-item.component";
import {Profile} from "@app/shared/models/profile";
@Component({
selector: 'app-horizental-profile-list',
standalone: true,
imports: [
HorizentalProfileItemComponent
],
templateUrl: './horizental-profile-list.component.html',
styleUrl: './horizental-profile-list.component.scss'
})
export class HorizentalProfileListComponent {
@Input({required:true}) profiles: Profile[] = []
}

View File

@@ -0,0 +1,4 @@
@if (profile != undefined) {
<h2 class="text-3xl font-extrabold text-black dark:text-white">{{ profile.profession | uppercase }}</h2>
<p class="leading-relaxed text-lg mb-4 dark:text-white">{{ profile.apropos }}</p>
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyHomeProfileComponent } from './my-home-profile.component';
describe('MyHomeProfileComponent', () => {
let component: MyHomeProfileComponent;
let fixture: ComponentFixture<MyHomeProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyHomeProfileComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MyHomeProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,20 @@
import {Component, Input} from '@angular/core';
import {AsyncPipe, JsonPipe, UpperCasePipe} from "@angular/common";
import {Profile} from "@app/shared/models/profile";
import {UntilDestroy} from "@ngneat/until-destroy";
@Component({
selector: 'app-my-home-profile',
standalone: true,
imports: [
UpperCasePipe,
AsyncPipe,
JsonPipe
],
templateUrl: './my-home-profile.component.html',
styleUrl: './my-home-profile.component.scss'
})
@UntilDestroy()
export class MyHomeProfileComponent {
@Input({required: true}) profile: Profile | undefined = undefined
}

View File

@@ -0,0 +1,25 @@
@if (project) {
<div class="bg-white rounded-2xl border p-6 max-w-sm">
<div class="">
<h3 class="text-lg font-bold text-gray-800 mb-3">{{ project.nom }}</h3>
<p class="text-gray-800 text-sm">{{ project.description }}</p>
<div class="mt-6">
<a [routerLink]="[]"
class="flex items-center flex-wrap justify-between gap-2 border rounded-3xl pl-5 pr-3 h-14 w-full hover:bg-purple-100 transition-all duration-300">
Modifier
<div class="w-11 h-11 rounded-full bg-purple-200 flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 cursor-pointer">
<path
d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32l8.4-8.4Z"/>
<path
d="M5.25 5.25a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3V13.5a.75.75 0 0 0-1.5 0v5.25a1.5 1.5 0 0 1-1.5 1.5H5.25a1.5 1.5 0 0 1-1.5-1.5V8.25a1.5 1.5 0 0 1 1.5-1.5h5.25a.75.75 0 0 0 0-1.5H5.25Z"/>
</svg>
</div>
</a>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileProjectItemComponent } from './my-profile-project-item.component';
describe('MyProfileProjectItemComponent', () => {
let component: MyProfileProjectItemComponent;
let fixture: ComponentFixture<MyProfileProjectItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyProfileProjectItemComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MyProfileProjectItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,32 @@
import {Component, inject, Input, OnInit} from '@angular/core';
import {AuthService} from "@app/core/services/authentication/auth.service";
import {ProjectService} from "@app/core/services/project/project.service";
import {Project} from "@app/shared/models/project";
import {environment} from "@env/environment";
import {RouterLink} from "@angular/router";
@Component({
selector: 'app-my-profile-project-item',
standalone: true,
imports: [
RouterLink
],
templateUrl: './my-profile-project-item.component.html',
styleUrl: './my-profile-project-item.component.scss'
})
export class MyProfileProjectItemComponent implements OnInit {
protected readonly environment = environment;
@Input({required: true}) projectId: string = '';
protected authService = inject(AuthService);
protected projectService = inject(ProjectService);
protected project: Project | undefined = undefined
ngOnInit(): void {
this.projectService.getProjectById(this.projectId).subscribe(
value => this.project = value
);
}
}

View File

@@ -0,0 +1,35 @@
<div class="min-h-screen py-4 font-sans">
<div class="max-w-4xl max-lg:max-w-2xl max-sm:max-w-sm mx-auto">
<h2 class="text-2xl font-bold text-gray-800 mb-8">Mes projets</h2>
@if (projects) {
<div class="relative flex items-center">
<select [(ngModel)]="projectIdSelected"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]">
<option [value]="null" disabled>Selectionner le projet à modifier</option>
<option [value]="'add'.toLowerCase()">
Ajouter un nouveau projet
</option>
@for (project of projects; track project.id) {
<option [value]="project.id">
{{ project.nom }}
</option>
}
</select>
</div>
}
<div class="w-full my-8">
@if (projectIdSelected() != null) {
<app-my-profile-update-project-form [projectId]="projectIdSelected()"
(formIsUpdated)="onProjectFormSubmitted($event)"/>
}
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileProjectListComponent } from './my-profile-project-list.component';
describe('MyProfileProjectListComponent', () => {
let component: MyProfileProjectListComponent;
let fixture: ComponentFixture<MyProfileProjectListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyProfileProjectListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MyProfileProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,48 @@
import {Component, inject, Input, OnInit, signal} from '@angular/core';
import {
MyProfileProjectItemComponent
} from "@app/shared/components/my-profile-project-item/my-profile-project-item.component";
import {PaginatorModule} from "primeng/paginator";
import {ReactiveFormsModule} from "@angular/forms";
import {ProjectService} from "@app/core/services/project/project.service";
import {AsyncPipe, JsonPipe} from "@angular/common";
import {Project} from "@app/shared/models/project";
import {UntilDestroy} from "@ngneat/until-destroy";
import {
MyProfileUpdateProjectFormComponent
} from "@app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component";
@Component({
selector: 'app-my-profile-project-list',
standalone: true,
imports: [
MyProfileProjectItemComponent,
PaginatorModule,
ReactiveFormsModule,
AsyncPipe,
JsonPipe,
MyProfileUpdateProjectFormComponent
],
templateUrl: './my-profile-project-list.component.html',
styleUrl: './my-profile-project-list.component.scss'
})
@UntilDestroy()
export class MyProfileProjectListComponent implements OnInit {
@Input({required: true}) projectIds: string[] = [];
@Input({required: true}) userId: string = "";
protected projectService = inject(ProjectService);
protected projectIdSelected = signal<string|null>(null);
protected projects: Project[] = []
ngOnInit(): void {
this.projectService.getProjectByUserId(this.userId).subscribe(
value => this.projects = value
);
}
onProjectFormSubmitted($event: string | null) {
this.projectIdSelected.set(null)
}
}

View File

@@ -0,0 +1,41 @@
<div class="flex max-sm:flex-col flex-row max-w-sm:space-y-2 space-x-2 justify-around items-center">
@if (file !=null){
<div class="flex-col flex space-y-2 justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 cursor-pointer text-red-600" (click)="file=null" >
<title>Supprimer le fichier</title>
<path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" clip-rule="evenodd" />
</svg>
<img src="assets/images/pdf.svg" alt="pdf" class="max-w-sm max-h-16">
<small>{{file.name}}</small>
</div>
}
<label for="uploadFile1"
class="flex justify-center items-center space-x-2 bg-gray-800 hover:bg-gray-700 text-white text-base px-3 py-1 outline-none rounded w-max cursor-pointer font-[sans-serif]">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 mr-2 fill-white inline" viewBox="0 0 32 32">
<path
d="M23.75 11.044a7.99 7.99 0 0 0-15.5-.009A8 8 0 0 0 9 27h3a1 1 0 0 0 0-2H9a6 6 0 0 1-.035-12 1.038 1.038 0 0 0 1.1-.854 5.991 5.991 0 0 1 11.862 0A1.08 1.08 0 0 0 23 13a6 6 0 0 1 0 12h-3a1 1 0 0 0 0 2h3a8 8 0 0 0 .75-15.956z"
data-original="#000000"/>
<path
d="M20.293 19.707a1 1 0 0 0 1.414-1.414l-5-5a1 1 0 0 0-1.414 0l-5 5a1 1 0 0 0 1.414 1.414L15 16.414V29a1 1 0 0 0 2 0V16.414z"
data-original="#000000"/>
</svg>
<small class="text-xs">Selectionner un fichier .pdf</small>
<input type="file" id='uploadFile1' class="hidden"
accept="application/pdf"
(change)="onFileChange($event)"/>
</label>
</div>
@if (file != null) {
<button type="button" [ngClass]="{'bg-purple-600':file!=null}"
class="!mt-2 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
(click)="onSubmit()">Mettre à jour mon CV
</button>
}

View File

@@ -0,0 +1,57 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileUpdateCvFormComponent } from './my-profile-update-cv-form.component';
import {ToastrService} from "ngx-toastr";
import {ProfileService} from "@app/core/services/profile/profile.service";
import {AuthService} from "@app/core/services/authentication/auth.service";
import {signal} from "@angular/core";
import {Auth} from "@app/shared/models/auth";
import {provideRouter} from "@angular/router";
describe('MyProfileUpdateCvFormComponent', () => {
let component: MyProfileUpdateCvFormComponent;
let fixture: ComponentFixture<MyProfileUpdateCvFormComponent>;
let mockToastrService: Partial<ToastrService>;
let mockProfileService: Partial<ProfileService>;
let mockAuthService: Partial<AuthService>;
beforeEach(async () => {
mockToastrService={
success: jest.fn(),
error: jest.fn(),
warning: jest.fn(),
}
mockProfileService = {
updateProfile: jest.fn().mockReturnValue({
subscribe: jest.fn()
})
};
mockAuthService = {
updateUser: jest.fn(),
user: signal<Auth|undefined>(undefined)
};
await TestBed.configureTestingModule({
imports: [MyProfileUpdateCvFormComponent],
providers: [
provideRouter([]),
{ provide: ToastrService, useValue: mockToastrService },
{ provide: ProfileService, useValue: mockProfileService },
{ provide: AuthService, useValue: mockAuthService }
]
})
.compileComponents();
fixture = TestBed.createComponent(MyProfileUpdateCvFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,62 @@
import {Component, inject, Input, output} from '@angular/core';
import {AuthService} from "@app/core/services/authentication/auth.service";
import {Profile} from "@app/shared/models/profile";
import {ProfileService} from "@app/core/services/profile/profile.service";
import {NgClass} from "@angular/common";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-my-profile-update-cv-form',
standalone: true,
imports: [
NgClass
],
templateUrl: './my-profile-update-cv-form.component.html',
styleUrl: './my-profile-update-cv-form.component.scss'
})
export class MyProfileUpdateCvFormComponent {
@Input({required: true}) profile: Profile | undefined = undefined;
private readonly profileService = inject(ProfileService);
private readonly toastrService = inject(ToastrService);
private readonly authService = inject(AuthService);
file: File | null = null; // Variable to store file
onSubmit() {
if (this.file != null) {
const formData = new FormData();
formData.append('cv', this.file); // "avatar" est le nom du champ dans PocketBase
this.profileService.updateProfile(this.profile?.id!, formData).subscribe(
value => {
this.authService.updateUser();
this.toastrService.success(
` Votre CV a bien été modifier !`,
`Mise à jour`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true
}
);
}
)
}
}
onFileChange($event: Event) {
const target: HTMLInputElement = $event.target as HTMLInputElement;
if (target?.files?.[0]) {
this.file = target.files[0];
}
}
}

View File

@@ -0,0 +1,183 @@
<form class="space-y-6 font-[sans-serif] max-w-md " [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white mb-4">Mon curriculum vitae (CV)</h3>
<app-my-profile-update-cv-form [profile]="profile"/>
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white">Ce qu'il faut savoir de moi</h3>
<div class="mx-8">
<div>
<label class="mb-2 text-sm text-black block dark:text-white">Biographie</label>
<div class="relative flex items-center">
<textarea placeholder='Type Message' formControlName="bio"
class="p-4 bg-white max-w-md mx-auto w-full block text-sm border border-gray-300 outline-[#007bff] rounded"
rows="4"></textarea>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white">A propos de vous</label>
<div class="relative flex items-center">
<textarea placeholder='Type Message' formControlName="apropos"
class="p-4 bg-white max-w-md mx-auto w-full block text-sm border border-gray-300 outline-[#007bff] rounded"
rows="4"></textarea>
</div>
</div>
</div>
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white">Mon domaine de competence</h3>
<div class="mx-8">
<div>
<label class="mb-2 text-sm text-black block dark:text-white">Profession</label>
<div class="relative flex items-center">
<input type='text' placeholder='votre metier' formControlName="profession"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M11 4V3a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v1H4a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1ZM9 2.5H7a.5.5 0 0 0-.5.5v1h3V3a.5.5 0 0 0-.5-.5ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" clip-rule="evenodd" />
<path d="M3 11.83V12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-.17c-.313.11-.65.17-1 .17H4c-.35 0-.687-.06-1-.17Z" />
</svg>
</div>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white">Secteur</label>
<div class="relative flex items-center">
<select formControlName="secteur" class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]">
<option [ngValue]="null" disabled>Selectionner votre secteur d'activité</option>
@for (sector of sectors(); track sector.id) {
<option [value]="sector.id">{{ sector.nom }}</option>
}
</select>
</div>
</div>
</div>
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white" >Mes réseaux </h3>
<div formGroupName="reseaux" class="mx-8">
<div>
<label class="mb-2 text-sm text-black block dark:text-white uppercase">Facebook</label>
<div class="relative flex items-center">
<input type='text' placeholder='lien vers votre facebook' formControlName="facebook"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<svg class="w-6 h-6" fill="#0866FF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Facebook</title>
<path
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/>
</svg>
</div>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white uppercase">Github</label>
<div class="relative flex items-center">
<input type='text' placeholder='Lien vers votre github' formControlName="github"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<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>
</div>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white uppercase">Instagram</label>
<div class="relative flex items-center">
<input type='text' placeholder='Lien vers votre instagram' formControlName="instagram"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<svg class="w-6 h-6" fill="#E4405F" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Instagram</title>
<path
d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"/>
</svg>
</div>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white uppercase">linkedIn</label>
<div class="relative flex items-center">
<input type='text' placeholder='Lien vers votre linkedIn' formControlName="linkedIn"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<svg class="w-6 h-6" fill="#0A66C2" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>LinkedIn</title>
<path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</div>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white uppercase">web</label>
<div class="relative flex items-center">
<input type='text' placeholder='Lien vers votre site web' formControlName="web"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<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>
</div>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white uppercase">x</label>
<div class="relative flex items-center">
<input type='text' placeholder='Lien vers votre compte X' formControlName="x"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<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>
</div>
</div>
</div>
<div>
<label class="mb-2 text-sm text-black block dark:text-white uppercase">YouTube</label>
<div class="relative flex items-center">
<input type='text' placeholder='Lien vers votre compte youtube' formControlName="youTube"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<svg class="w-6 h-6" fill="#FF0000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>YouTube</title>
<path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
</div>
</div>
</div>
</div>
<button type="submit" [ngClass]="{'bg-purple-600':profileForm.valid}"
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block">Sauvegarder
</button>
</form>

View File

@@ -0,0 +1,64 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MyProfileUpdateFormComponent} from './my-profile-update-form.component';
import {provideRouter} from "@angular/router";
import {ToastrService} from "ngx-toastr";
import {FormBuilder} from "@angular/forms";
describe('MyProfileUpdateFormComponent', () => {
let component: MyProfileUpdateFormComponent;
let fixture: ComponentFixture<MyProfileUpdateFormComponent>;
let mockToastrService : Partial<ToastrService>;
let mockProfileData = {profession:'',secteur:'',bio:'',apropos:'',reseaux:{facebook:'',github:'',instagram:'',linkedIn:'',web:'',x:'',youTube:''}};
beforeEach(async () => {
mockToastrService = {
warning: jest.fn(),
success: jest.fn(),
error: jest.fn()
};
await TestBed.configureTestingModule({
imports: [MyProfileUpdateFormComponent],
providers: [
FormBuilder,
provideRouter([]),
{ provide: ToastrService, useValue: mockToastrService }
]
})
.compileComponents();
fixture = TestBed.createComponent(MyProfileUpdateFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should submit invalid update form', () => {
component.profileForm.setValue(mockProfileData);
const spyUpdateProfile = jest.spyOn(component, 'onSubmit');
component.onSubmit();
expect(component.profileForm.valid).toEqual(false);
expect(spyUpdateProfile).toHaveBeenCalled();
});
it('should submit valid update form', () => {
mockProfileData.profession = 'developer';
mockProfileData.secteur = 'technology';
mockProfileData.bio = 'A passionate developer';
mockProfileData.apropos = 'About me';
component.profileForm.setValue(mockProfileData);
const spyUpdateProfile = jest.spyOn(component, 'onSubmit');
component.onSubmit();
expect(component.profileForm.valid).toEqual(true);
expect(spyUpdateProfile).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,101 @@
import {Component, inject, Input, OnInit, signal} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {UntilDestroy} from "@ngneat/until-destroy";
import {Profile} from "@app/shared/models/profile";
import {NgClass} from "@angular/common";
import {SectorService} from "@app/core/services/sector/sector.service";
import {Sector} from "@app/shared/models/sector";
import {ProfileService} from "@app/core/services/profile/profile.service";
import {AuthService} from "@app/core/services/authentication/auth.service";
import {
MyProfileUpdateCvFormComponent
} from "@app/shared/components/my-profile-update-cv-form/my-profile-update-cv-form.component";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-my-profile-update-form',
standalone: true,
imports: [
ReactiveFormsModule,
NgClass,
MyProfileUpdateCvFormComponent
],
templateUrl: './my-profile-update-form.component.html',
styleUrl: './my-profile-update-form.component.scss'
})
@UntilDestroy()
export class MyProfileUpdateFormComponent implements OnInit {
private readonly toastrService = inject(ToastrService);
@Input({required: true}) profile: Profile = {} as Profile;
private readonly formBuilder = inject(FormBuilder);
protected readonly sectorService = inject(SectorService);
protected readonly profileService = inject(ProfileService);
protected readonly authService = inject(AuthService);
profileForm!: FormGroup;
protected sectors = signal<Sector[]>([]);
ngOnInit(): void {
this.profileForm = this.formBuilder.group({
profession: new FormControl(this.profile.profession?this.profile.profession.toLowerCase():'', [Validators.required]),
secteur: new FormControl<string|null>(this.profile.secteur ? this.profile.secteur.toLowerCase() : null, [Validators.required]),
bio: new FormControl(this.profile.bio ? this.profile.bio.toLowerCase() : ''),
apropos: new FormControl(this.profile.apropos ? this.profile.apropos.toLowerCase() : ''),
reseaux: new FormGroup({
facebook: new FormControl(this.profile.reseaux ? (this.profile.reseaux as any)["facebook"] : ''),
github: new FormControl(this.profile.reseaux ?(this.profile.reseaux as any)["github"] : ''),
instagram: new FormControl(this.profile.reseaux ? (this.profile.reseaux as any)["instagram"] : ''),
linkedIn: new FormControl(this.profile.reseaux ? (this.profile.reseaux as any)["linkedIn"] : ''),
web: new FormControl(this.profile.reseaux ? (this.profile.reseaux as any)["web"]: ''),
x: new FormControl(this.profile.reseaux ? (this.profile.reseaux as any)["x"] : ''),
youTube: new FormControl(this.profile.reseaux ? (this.profile.reseaux as any)["youTube"] : '')
})
});
if (this.profile.secteur) {
this.sectorService.getSectorById(this.profile.secteur).subscribe(value => this.profileForm.get("secteur")!.setValue(value.id));
}
this.sectorService.sectors.subscribe(value => this.sectors.set(value));
}
onSubmit() {
if (this.profileForm.invalid) {
return;
}
const data: Profile = {
profession: this.profileForm.getRawValue().profession,
secteur: this.profileForm.getRawValue().secteur,
apropos: this.profileForm.getRawValue().apropos,
bio: this.profileForm.getRawValue().bio,
reseaux: this.profileForm.getRawValue().reseaux
} as Profile
this.profileService.updateProfile(this.profile.id, data).subscribe({
next: (value) => {
this.authService.updateUser();
this.toastrService.success(
` Vos informations personnelles ont bien été modifier !`,
`Mise à jour`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true
}
);
},
error: (error) => {
this.toastrService.error("Une erreur est survenue lors de la mise à jour de votre profil", "Erreur");
}
}
);
}
}

View File

@@ -0,0 +1,62 @@
@if (projectId) {
@if (projectId == 'add'.toLowerCase()) {
<app-project-picture-form [project]="undefined"/>
} @else {
<app-project-picture-form [project]="project"/>
}
<h3 class="font-ubuntu w-full text-start font-bold text-xl uppercase dark:text-white my-5">Information du
projet </h3>
<form class="mx-8" [formGroup]="projectForm" (ngSubmit)="onSubmit()">
<label class="mb-2 text-sm text-black block dark:text-white">Nom</label>
<div class="relative flex items-center">
<input type='text' placeholder='nom du projet' formControlName="nom"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<svg xmlns="http://www.w3.org/2000/svg" width="22px" height="22px" fill="#bbb" viewBox="0 0 512 512">
<path
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
data-original="#000000"></path>
</svg>
</div>
</div>
<label class="mb-2 text-sm text-black block dark:text-white">Lien</label>
<div class="relative flex items-center">
<input type='text' placeholder='lien vers votre projet ex : http://monprojet' formControlName="lien"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"/>
<div class="absolute left-4">
<svg xmlns="http://www.w3.org/2000/svg" width="22px" height="22px" fill="#bbb" viewBox="0 0 512 512">
<path
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
data-original="#000000"></path>
</svg>
</div>
</div>
<label class="mb-2 text-sm text-black block dark:text-white">description</label>
<div class="relative flex items-center">
<textarea placeholder='Type Message' formControlName="description"
class="p-4 bg-white w-full block text-sm border border-gray-300 outline-[#007bff] rounded"
rows="4"></textarea>
</div>
<button type="submit" [ngClass]="{'bg-purple-600':projectForm.valid}"
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block">Sauvegarder
</button>
<button type="button" (click)="formIsUpdated.emit(null)"
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block">Annuler
</button>
</form>
}

Some files were not shown because too many files have changed in this diff Show More