committed by
styve Lioumba
parent
1dc1109482
commit
4fb600b0cb
26
angular.json
26
angular.json
@@ -17,7 +17,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/trouve-ton-profile",
|
||||
"outputPath": "dist/",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
@@ -33,14 +33,13 @@
|
||||
"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": {
|
||||
"entry": "server.ts"
|
||||
}
|
||||
"ssr": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -89,25 +88,6 @@
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
compose.yaml
Normal file
22
compose.yaml
Normal 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
16
jest.config.js
Normal 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',
|
||||
},
|
||||
};
|
||||
10645
package-lock.json
generated
10645
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -4,9 +4,12 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start:dev": "docker compose up -d && ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"serve:ssr:TrouveTonProfile": "node dist/trouve-ton-profile/server/server.mjs"
|
||||
},
|
||||
"private": true,
|
||||
@@ -26,10 +29,15 @@
|
||||
"@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"
|
||||
@@ -40,14 +48,13 @@
|
||||
"@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",
|
||||
"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",
|
||||
"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"
|
||||
|
||||
@@ -7,6 +7,9 @@ main {
|
||||
.content {
|
||||
flex: 1; /* Cette zone grandit pour remplir l'espace restant */
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
app-nav-bar {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {authGuard} from "@app/core/guard/authentication/auth.guard";
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -24,6 +25,7 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'my-profile',
|
||||
title: 'Mon profile',
|
||||
canActivate: [authGuard],
|
||||
loadChildren: () => import('@app/routes/my-profile/my-profile.module').then(m => m.MyProfileModule)
|
||||
},
|
||||
{
|
||||
|
||||
58
src/app/core/guard/authentication/auth.guard.spec.ts
Normal file
58
src/app/core/guard/authentication/auth.guard.spec.ts
Normal 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");
|
||||
|
||||
});
|
||||
});
|
||||
13
src/app/core/guard/authentication/auth.guard.ts
Normal file
13
src/app/core/guard/authentication/auth.guard.ts
Normal 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;
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
9
src/app/core/resolvers/my-profile/my-profile.resolver.ts
Normal file
9
src/app/core/resolvers/my-profile/my-profile.resolver.ts
Normal 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}
|
||||
};
|
||||
@@ -1,17 +1,67 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
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', () => {
|
||||
const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => detailResolver(...resolverParameters));
|
||||
|
||||
let mockRoute: Partial<Router>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
mockRoute = {
|
||||
getCurrentNavigation: jest.fn()
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Router, useValue: mockRoute }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
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();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ResolveFn } from '@angular/router';
|
||||
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<string> = (route, state) => {
|
||||
export const detailResolver: ResolveFn<{ user:User,profile:Profile }> = (route, state) => {
|
||||
const paramValue = route.params['name'];
|
||||
return "profile-list-resolver works!, paramValue: " + paramValue || "no query value found!";
|
||||
const router = inject(Router);
|
||||
return router.getCurrentNavigation()?.extras.state as { user:User,profile:Profile }
|
||||
};
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
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', () => {
|
||||
const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => listResolver(...resolverParameters));
|
||||
|
||||
let mockProfileService: Partial<ProfileService>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
|
||||
mockProfileService = {
|
||||
profiles: of([] as Profile[])
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ProfileService, useValue: mockProfileService }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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<string> = (route, state) => {
|
||||
export const listResolver: ResolveFn<Observable<Profile[]>> = (route, state) => {
|
||||
const queryValue = route.queryParams['search'];
|
||||
return "profile-list-resolver works!, queryValue: " + queryValue || "no query value found!";
|
||||
const profileService = inject(ProfileService);
|
||||
|
||||
return profileService.profiles;
|
||||
};
|
||||
|
||||
76
src/app/core/services/authentication/auth.service.spec.ts
Normal file
76
src/app/core/services/authentication/auth.service.spec.ts
Normal 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());
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
71
src/app/core/services/authentication/auth.service.ts
Normal file
71
src/app/core/services/authentication/auth.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
16
src/app/core/services/profile/profile.service.spec.ts
Normal file
16
src/app/core/services/profile/profile.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
40
src/app/core/services/profile/profile.service.ts
Normal file
40
src/app/core/services/profile/profile.service.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
16
src/app/core/services/project/project.service.spec.ts
Normal file
16
src/app/core/services/project/project.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
34
src/app/core/services/project/project.service.ts
Normal file
34
src/app/core/services/project/project.service.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
16
src/app/core/services/sector/sector.service.spec.ts
Normal file
16
src/app/core/services/sector/sector.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
26
src/app/core/services/sector/sector.service.ts
Normal file
26
src/app/core/services/sector/sector.service.ts
Normal 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))
|
||||
}
|
||||
|
||||
}
|
||||
16
src/app/core/services/user/user.service.spec.ts
Normal file
16
src/app/core/services/user/user.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
21
src/app/core/services/user/user.service.ts
Normal file
21
src/app/core/services/user/user.service.ts
Normal 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
@@ -1,6 +1,7 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthComponent } from './auth.component';
|
||||
import {provideRouter} from "@angular/router";
|
||||
|
||||
describe('AuthComponent', () => {
|
||||
let component: AuthComponent;
|
||||
@@ -8,7 +9,10 @@ describe('AuthComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AuthComponent]
|
||||
imports: [AuthComponent],
|
||||
providers:[
|
||||
provideRouter([])
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
||||
@@ -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">
|
||||
<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 dark:text-white text-3xl md:text-5xl lg:text-6xl">
|
||||
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 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 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 max-w-3xl space-y-2">
|
||||
<div class="mt-8 mx-auto w-full 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>
|
||||
|
||||
|
||||
@@ -1,46 +1,67 @@
|
||||
#container {
|
||||
color:#999;
|
||||
text-transform: uppercase;
|
||||
font-size:36px;
|
||||
font-weight:bold;
|
||||
display:block;
|
||||
}
|
||||
|
||||
#flip {
|
||||
margin-top: 10px;
|
||||
height:50px;
|
||||
h1 {
|
||||
.word-animation {
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
color: #4ec7f3;
|
||||
display: block;
|
||||
text-transform: capitalize;
|
||||
animation: rotateSpin 10s infinite;
|
||||
|
||||
&.red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#flip > div > div {
|
||||
color:#fff;
|
||||
padding:4px 12px;
|
||||
height:45px;
|
||||
margin-bottom:45px;
|
||||
display:inline-block;
|
||||
&.orange {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
#flip div:first-child {
|
||||
animation: show 10s linear infinite;
|
||||
&.blue {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#flip div div {
|
||||
background:#42c58a;
|
||||
}
|
||||
#flip div:first-child div {
|
||||
background:#4ec7f3;
|
||||
}
|
||||
#flip div:last-child div {
|
||||
background:#DC143C;
|
||||
@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%);
|
||||
}
|
||||
|
||||
@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;}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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}
|
||||
{
|
||||
path: '', component: MyProfileComponent, resolve: {user: myProfileResolver}
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -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. N’hésitez pas à me contacter pour en savoir plus sur mon parcours et mes domaines
|
||||
d’intervention.</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>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MyProfileComponent } from './my-profile.component';
|
||||
import {provideRouter} from "@angular/router";
|
||||
|
||||
describe('MyProfileComponent', () => {
|
||||
let component: MyProfileComponent;
|
||||
@@ -8,7 +9,10 @@ describe('MyProfileComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MyProfileComponent]
|
||||
imports: [MyProfileComponent],
|
||||
providers: [
|
||||
provideRouter([])
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
||||
@@ -1,12 +1,87 @@
|
||||
import { Component } from '@angular/core';
|
||||
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: [],
|
||||
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'
|
||||
})
|
||||
export class MyProfileComponent {
|
||||
@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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,81 @@
|
||||
<section class="text-gray-600 body-font">
|
||||
<div class="container px-5 py-24 mx-auto flex flex-col">
|
||||
<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 h-64 overflow-hidden">
|
||||
<img alt="content" class="object-cover object-center h-full w-full" src="https://dummyimage.com/1200x500">
|
||||
<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">
|
||||
<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>
|
||||
@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">
|
||||
<h2 class="font-medium title-font mt-4 text-gray-900 text-lg">Phoebe Caulfield</h2>
|
||||
@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>
|
||||
<p class="text-base">Raclette knausgaard hella meggs normcore williamsburg enamel pin sartorial venmo tbh hot chicken gentrify portland.</p>
|
||||
@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. N’hésitez pas à me contacter pour en savoir plus sur mon parcours et mes domaines
|
||||
d’intervention.</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>
|
||||
<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>
|
||||
}
|
||||
|
||||
@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 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
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ProfileDetailComponent} from './profile-detail.component';
|
||||
import {provideRouter} from "@angular/router";
|
||||
|
||||
describe('ProfileDetailComponent', () => {
|
||||
let component: ProfileDetailComponent;
|
||||
@@ -8,7 +9,10 @@ describe('ProfileDetailComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProfileDetailComponent]
|
||||
imports: [ProfileDetailComponent],
|
||||
providers:[
|
||||
provideRouter([])
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {Component, computed, inject} from '@angular/core';
|
||||
import {ActivatedRoute, RouterLink} from "@angular/router";
|
||||
import {QRCodeModule} from "angularx-qrcode";
|
||||
import {SafeUrl} from "@angular/platform-browser";
|
||||
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
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
<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">
|
||||
<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">
|
||||
<app-display-profile-card (onDisplayChange)="showNewDisplay($event)"/>
|
||||
|
||||
@switch (display()) {
|
||||
@case ('list'.toUpperCase()) {
|
||||
<app-horizental-profile-list/>
|
||||
<app-horizental-profile-list [profiles]="profiles"/>
|
||||
}
|
||||
@case ('grid'.toUpperCase()) {
|
||||
<app-vertical-profile-list/>
|
||||
<app-vertical-profile-list [profiles]="profiles"/>
|
||||
}
|
||||
@default {
|
||||
<app-vertical-profile-list/>
|
||||
<app-vertical-profile-list [profiles]="profiles"/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProfileListComponent } from './profile-list.component';
|
||||
import {provideRouter} from "@angular/router";
|
||||
|
||||
describe('ProfileListComponent', () => {
|
||||
let component: ProfileListComponent;
|
||||
@@ -8,7 +9,10 @@ describe('ProfileListComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProfileListComponent]
|
||||
imports: [ProfileListComponent],
|
||||
providers:[
|
||||
provideRouter([])
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
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',
|
||||
@@ -31,16 +33,13 @@ import {
|
||||
templateUrl: './profile-list.component.html',
|
||||
styleUrl: './profile-list.component.scss'
|
||||
})
|
||||
@UntilDestroy()
|
||||
export class ProfileListComponent {
|
||||
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
protected profiles = this.route.snapshot.data['profiles'];
|
||||
protected profiles : Profile[] = this.route.snapshot.data['profiles'] as Profile[];
|
||||
protected display = signal<string>('grid'.toUpperCase());
|
||||
|
||||
constructor() {
|
||||
console.log(this.profiles)
|
||||
}
|
||||
|
||||
showNewDisplay($event: string) {
|
||||
this.display.set($event.toUpperCase())
|
||||
}
|
||||
|
||||
10
src/app/shared/components/chips/chips.component.html
Normal file
10
src/app/shared/components/chips/chips.component.html
Normal 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>
|
||||
}
|
||||
|
||||
|
||||
23
src/app/shared/components/chips/chips.component.spec.ts
Normal file
23
src/app/shared/components/chips/chips.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
30
src/app/shared/components/chips/chips.component.ts
Normal file
30
src/app/shared/components/chips/chips.component.ts
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<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">
|
||||
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">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
@@ -8,7 +9,10 @@ describe('FooterComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FooterComponent]
|
||||
imports: [FooterComponent],
|
||||
providers:[
|
||||
provideRouter([]),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
|
||||
@@ -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>
|
||||
@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>
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
import {Component, inject, Input} from '@angular/core';
|
||||
import {Router} from "@angular/router";
|
||||
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: [],
|
||||
imports: [
|
||||
RouterLink,
|
||||
ChipsComponent,
|
||||
ReseauxComponent
|
||||
],
|
||||
templateUrl: './horizental-profile-item.component.html',
|
||||
styleUrl: './horizental-profile-item.component.scss'
|
||||
})
|
||||
export class HorizentalProfileItemComponent {
|
||||
export class HorizentalProfileItemComponent implements OnInit {
|
||||
|
||||
@Input() user: any = {};
|
||||
@Input({required: true}) profile: Profile = {} as Profile;
|
||||
protected router = inject(Router)
|
||||
protected userService = inject(UserService);
|
||||
|
||||
onShowDetail(user: any) {
|
||||
this.router.navigate(['/profiles', "user1"])
|
||||
protected user: User | undefined = undefined;
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userService.getUserById(this.profile.utilisateur).subscribe(
|
||||
value => this.user = value
|
||||
)
|
||||
}
|
||||
|
||||
protected readonly environment = environment;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
<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/>
|
||||
@for (profile of profiles; track profile.id) {
|
||||
<app-horizental-profile-item [profile]="profile"/>
|
||||
} @empty {
|
||||
<p>Aucun profile trouvée</p>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
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',
|
||||
@@ -13,5 +14,5 @@ import {
|
||||
styleUrl: './horizental-profile-list.component.scss'
|
||||
})
|
||||
export class HorizentalProfileListComponent {
|
||||
|
||||
@Input({required:true}) profiles: Profile[] = []
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MyProfileUpdateProjectFormComponent } from './my-profile-update-project-form.component';
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ProjectService} from "@app/core/services/project/project.service";
|
||||
import {provideRouter} from "@angular/router";
|
||||
import {signal} from "@angular/core";
|
||||
import {Auth} from "@app/shared/models/auth";
|
||||
|
||||
describe('MyProfileUpdateProjectFormComponent', () => {
|
||||
let component: MyProfileUpdateProjectFormComponent;
|
||||
let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>;
|
||||
|
||||
let mockProjectService: Partial<ProjectService>;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
let mockToastrService: Partial<ToastrService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
mockToastrService={
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn()
|
||||
}
|
||||
|
||||
mockAuthService = {
|
||||
user: signal<Auth|undefined>(undefined),
|
||||
};
|
||||
|
||||
mockProjectService={}
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MyProfileUpdateProjectFormComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
{ provide: ProjectService, useValue: mockProjectService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MyProfileUpdateProjectFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import {Component, inject, Input, OnChanges, OnInit, output, SimpleChanges} from '@angular/core';
|
||||
import {FormBuilder, FormControl, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {ProjectService} from "@app/core/services/project/project.service";
|
||||
import {Project} from "@app/shared/models/project";
|
||||
import {NgClass} from "@angular/common";
|
||||
import {PaginatorModule} from "primeng/paginator";
|
||||
import {ProjectPictureFormComponent} from "@app/shared/components/project-picture-form/project-picture-form.component";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
import {ProjectDto} from "@app/shared/models/project-dto";
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-profile-update-project-form',
|
||||
standalone: true,
|
||||
imports: [
|
||||
PaginatorModule,
|
||||
ReactiveFormsModule,
|
||||
NgClass,
|
||||
ProjectPictureFormComponent
|
||||
],
|
||||
templateUrl: './my-profile-update-project-form.component.html',
|
||||
styleUrl: './my-profile-update-project-form.component.scss'
|
||||
})
|
||||
export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input({required: true}) projectId: string | null = null;
|
||||
|
||||
protected project: Project | undefined = undefined
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
protected readonly projectService = inject(ProjectService);
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
|
||||
protected projectForm = this.formBuilder.group({
|
||||
nom: new FormControl('', [Validators.required]),
|
||||
description: new FormControl('', [Validators.required]),
|
||||
lien: new FormControl('')
|
||||
});
|
||||
|
||||
formIsUpdated = output<string | null>({alias: 'formIsUpdated'})
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.projectId == 'add'.toLowerCase()){
|
||||
this.projectForm.setValue({
|
||||
nom: '',
|
||||
description: '',
|
||||
lien: ''
|
||||
})
|
||||
}
|
||||
|
||||
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
|
||||
|
||||
this.projectService.getProjectById(this.projectId).subscribe(
|
||||
value => {
|
||||
this.project = value;
|
||||
this.projectForm.setValue({
|
||||
nom: value.nom,
|
||||
description: value.description,
|
||||
lien: value.lien
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.projectForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
|
||||
|
||||
// Update
|
||||
|
||||
this.projectService.updateProject(this.project!.id, this.projectForm.getRawValue()).subscribe(value => {
|
||||
|
||||
this.formIsUpdated.emit(value.id);
|
||||
|
||||
this.toastrService.success(
|
||||
`Les informations du projet ${value.nom} ont bien été modifier !`,
|
||||
`Mise à jour`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true
|
||||
}
|
||||
);
|
||||
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
// Create
|
||||
|
||||
const projectDto: ProjectDto = {
|
||||
...this.projectForm.getRawValue(),
|
||||
utilisateur: this.authService.user()!.record!.id
|
||||
} as ProjectDto;
|
||||
|
||||
|
||||
this.projectService.createProject(projectDto).subscribe(value => {
|
||||
|
||||
this.formIsUpdated.emit(value.id);
|
||||
|
||||
this.toastrService.success(
|
||||
`Le projet ${value.nom} a bien été créer !`,
|
||||
`Nouveau projet`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.projectId = changes['projectId'].currentValue;
|
||||
this.ngOnInit();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,6 +11,25 @@
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex md:order-2 space-x-1 items-center">
|
||||
|
||||
@if (authService.isAuthenticated() && authService.isEmailVerified()) {
|
||||
|
||||
@if (authService.user()!.record!; as user) {
|
||||
<a [routerLink]="['my-profile']" [state]="{user}" class="w-10 h-10 dark:border-white dark:border 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">
|
||||
}
|
||||
</a>
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
<button
|
||||
type="button"
|
||||
@@ -19,7 +38,8 @@
|
||||
<span class="inline-block h-4 w-4">
|
||||
<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="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/>
|
||||
|
||||
</svg>
|
||||
|
||||
@@ -42,6 +62,22 @@
|
||||
</button>
|
||||
}
|
||||
<span class="text-black dark:text-white"> | </span>
|
||||
|
||||
|
||||
@if (authService.user()?.isValid) {
|
||||
<a [routerLink]="['/auth']" (click)="authService.logout(); authService.updateUser();"
|
||||
class="text-black dark:text-white font-medium rounded-lg text-sm px-4 py-2 text-center flex items-center justify-center space-x-4">
|
||||
<span class="h-4 w-4">
|
||||
<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="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75"/>
|
||||
</svg>
|
||||
|
||||
</span>
|
||||
<span class="hidden sm:block text-black text-center dark:text-white">Se deconnecter</span>
|
||||
</a>
|
||||
} @else {
|
||||
<a [routerLink]="['/auth']"
|
||||
class="text-black dark:text-white font-medium rounded-lg text-sm px-4 py-2 text-center flex items-center justify-center gap-1">
|
||||
<span class="inline-block h-4 w-4">
|
||||
@@ -55,18 +91,31 @@
|
||||
</span>
|
||||
<span class="hidden sm:block text-black dark:text-white">Se connecter</span>
|
||||
</a>
|
||||
<button
|
||||
}
|
||||
|
||||
<!--<button
|
||||
data-collapse-toggle="navbar-sticky"
|
||||
(click)="onOpenMenu()"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
aria-controls="navbar-sticky"
|
||||
aria-expanded="false">
|
||||
@if (isMenuOpen) {
|
||||
|
||||
<span class="sr-only">Close main menu</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 dark:text-white">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"
|
||||
clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
} @else {
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
class="w-5 h-5 dark:text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 17 14">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
@@ -75,9 +124,20 @@
|
||||
stroke-width="2"
|
||||
d="M1 1h15M1 7h15M1 13h15"/>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</button>-->
|
||||
</div>
|
||||
</div>
|
||||
<!-- @if (isMenuOpen) {
|
||||
<div class="w-full flex justify-end px-2">
|
||||
<a [routerLink]="['my-profile']" class="flex w-max items-center space-x-2 border px-2 py-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 dark:text-white">
|
||||
<path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="dark:text-white">Mon profil</span>
|
||||
</a>
|
||||
</div>
|
||||
}-->
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NavBarComponent } from './nav-bar.component';
|
||||
import {ThemeService} from "@app/core/services/theme/theme.service";
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
import { provideRouter } from '@angular/router';
|
||||
import {signal} from "@angular/core";
|
||||
import {Auth} from "@app/shared/models/auth";
|
||||
import {User} from "@app/shared/models/user";
|
||||
|
||||
describe('NavBarComponent', () => {
|
||||
let component: NavBarComponent;
|
||||
let fixture: ComponentFixture<NavBarComponent>;
|
||||
let mockThemeService: Partial<ThemeService>;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
|
||||
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: ""
|
||||
};
|
||||
const mockUser : Auth = {isValid:false, record: user, token: "mockToken123"} as Auth;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockAuthService={
|
||||
updateUser: jest.fn(),
|
||||
user: signal<Auth | undefined>(mockUser),
|
||||
isAuthenticated: jest.fn().mockReturnValue(true),
|
||||
isEmailVerified: jest.fn().mockReturnValue(true)
|
||||
}
|
||||
mockThemeService = {
|
||||
darkModeSignal: signal<string>('null'),
|
||||
updateDarkMode: jest.fn()
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NavBarComponent]
|
||||
imports: [NavBarComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: ThemeService, useValue: mockThemeService },
|
||||
{ provide: AuthService, useValue: mockAuthService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
@@ -20,4 +57,17 @@ describe('NavBarComponent', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should call authService.updateUser on ngOnInit', () => {
|
||||
expect(mockAuthService.updateUser).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call themeService.updateDarkMode when toggleDarkMode called', () => {
|
||||
component.toggleDarkMode();
|
||||
expect(mockThemeService.updateDarkMode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {RouterLink} from "@angular/router";
|
||||
import {ThemeService} from "@app/core/services/theme/theme.service";
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
import {UntilDestroy} from "@ngneat/until-destroy";
|
||||
import {environment} from "@env/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-bar',
|
||||
@@ -11,10 +14,19 @@ import {ThemeService} from "@app/core/services/theme/theme.service";
|
||||
templateUrl: './nav-bar.component.html',
|
||||
styleUrl: './nav-bar.component.scss'
|
||||
})
|
||||
export class NavBarComponent {
|
||||
@UntilDestroy()
|
||||
export class NavBarComponent implements OnInit {
|
||||
protected themeService: ThemeService = inject(ThemeService);
|
||||
protected authService = inject(AuthService);
|
||||
|
||||
|
||||
toggleDarkMode() {
|
||||
this.themeService.updateDarkMode();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.authService.updateUser();
|
||||
}
|
||||
|
||||
protected readonly environment = environment;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
@if (project) {
|
||||
<div class="bg-white rounded-2xl border p-6">
|
||||
|
||||
@if (project.fichier) {
|
||||
<img alt="{{project.nom}}" class="object-cover object-center h-full w-full "
|
||||
src="{{environment.baseUrl}}/api/files/projets/{{project.id}}/{{project.fichier}}" loading="lazy">
|
||||
} @else {
|
||||
<img alt="nouveau-projet" class="object-cover object-center h-full w-full "
|
||||
src="https://api.dicebear.com/9.x/shapes/svg?seed={{project.nom}}" loading="lazy">
|
||||
}
|
||||
|
||||
<div class="mt-6">
|
||||
<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 class="text-indigo-500 inline-flex items-center md:mb-2 lg:mb-0" href="{{project.lien}}" target="_blank">Explore
|
||||
<svg class="w-4 h-4 ml-2" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectItemComponent } from './project-item.component';
|
||||
|
||||
describe('ProjectItemComponent', () => {
|
||||
let component: ProjectItemComponent;
|
||||
let fixture: ComponentFixture<ProjectItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProjectItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProjectItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {Project} from "@app/shared/models/project";
|
||||
import {JsonPipe} from "@angular/common";
|
||||
import {environment} from "@env/environment";
|
||||
import {RouterLink} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-item',
|
||||
standalone: true,
|
||||
imports: [
|
||||
JsonPipe,
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './project-item.component.html',
|
||||
styleUrl: './project-item.component.scss'
|
||||
})
|
||||
export class ProjectItemComponent {
|
||||
protected readonly environment = environment;
|
||||
|
||||
@Input({required: true}) project: Project | undefined = undefined;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<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">Explorer les projets</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
|
||||
@for (project of projects; track project) {
|
||||
<app-project-item [project]="project"/>
|
||||
} @empty {
|
||||
<p>Aucun projet</p>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectListComponent } from './project-list.component';
|
||||
|
||||
describe('ProjectListComponent', () => {
|
||||
let component: ProjectListComponent;
|
||||
let fixture: ComponentFixture<ProjectListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProjectListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProjectListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import {Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {UntilDestroy} from "@ngneat/until-destroy";
|
||||
import {ProjectItemComponent} from "@app/shared/components/project-item/project-item.component";
|
||||
import {JsonPipe} from "@angular/common";
|
||||
import {ProjectService} from "@app/core/services/project/project.service";
|
||||
import {Project} from "@app/shared/models/project";
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-list',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ProjectItemComponent,
|
||||
JsonPipe
|
||||
],
|
||||
templateUrl: './project-list.component.html',
|
||||
styleUrl: './project-list.component.scss'
|
||||
})
|
||||
@UntilDestroy()
|
||||
export class ProjectListComponent implements OnInit{
|
||||
@Input({required: true}) userProjectId: string = "";
|
||||
|
||||
protected readonly projectService = inject(ProjectService);
|
||||
|
||||
protected projects: Project[] = []
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectService.getProjectByUserId(this.userProjectId).subscribe(
|
||||
value => this.projects = value
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<div class="w-full text-center">
|
||||
<h3 class="font-ubuntu w-full text-start font-bold text-xl uppercase dark:text-white mb-4">Aperçu du projet </h3>
|
||||
|
||||
<div class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400">
|
||||
@if (imagePreviewUrl != null && project != undefined) {
|
||||
<img alt="nouveau-projet" class="object-cover object-center h-full w-full"
|
||||
[src]="imagePreviewUrl" loading="lazy">
|
||||
} @else if (project != undefined) {
|
||||
|
||||
@if (project.fichier) {
|
||||
<img alt="{{project!.nom}}" class="object-cover object-center h-full w-full "
|
||||
src="{{environment.baseUrl}}/api/files/projets/{{project.id}}/{{project.fichier}}" loading="lazy">
|
||||
} @else {
|
||||
<img alt="nouveau-projet" class="object-cover object-center h-full w-full "
|
||||
src="https://api.dicebear.com/9.x/shapes/svg?seed={{project.nom}}" loading="lazy">
|
||||
}
|
||||
}
|
||||
|
||||
@if (project == undefined) {
|
||||
<img alt="nouveau-projet" class="object-cover object-center h-full w-full "
|
||||
src="https://api.dicebear.com/9.x/shapes/svg?seed=nouveau-projet" loading="lazy">
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<label for="uploadFile1"
|
||||
class="flex bg-gray-800 hover:bg-gray-700 text-white text-base px-5 py-3 outline-none rounded w-max cursor-pointer mx-auto font-[sans-serif]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 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 une image</small>
|
||||
<input type="file" id='uploadFile1' class="hidden"
|
||||
accept="image/*"
|
||||
(change)="onPictureChange($event)"/>
|
||||
</label>
|
||||
|
||||
@if (file != null || imagePreviewUrl != null) {
|
||||
<button type="button" [ngClass]="{'bg-purple-600':file!=null || imagePreviewUrl != 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 ma photo de projet
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectPictureFormComponent } from './project-picture-form.component';
|
||||
import {provideRouter} from "@angular/router";
|
||||
import {ProjectService} from "@app/core/services/project/project.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
|
||||
describe('ProjectPictureFormComponent', () => {
|
||||
let component: ProjectPictureFormComponent;
|
||||
let fixture: ComponentFixture<ProjectPictureFormComponent>;
|
||||
|
||||
let mockProjectService: Partial<ProjectService>;
|
||||
let mockToastrService: Partial<ToastrService>;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockProjectService = {
|
||||
updateProject: jest.fn().mockReturnValue({
|
||||
subscribe: jest.fn()
|
||||
})
|
||||
};
|
||||
mockToastrService={
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
}
|
||||
mockAuthService = {
|
||||
updateUser: jest.fn()
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProjectPictureFormComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: ProjectService, useValue: mockProjectService },
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
{ provide: AuthService, useValue: mockAuthService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProjectPictureFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import {Component, inject, Input, output} from '@angular/core';
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
import {Project} from "@app/shared/models/project";
|
||||
import {ProjectService} from "@app/core/services/project/project.service";
|
||||
import {NgClass} from "@angular/common";
|
||||
import {environment} from "@env/environment";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-picture-form',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './project-picture-form.component.html',
|
||||
styleUrl: './project-picture-form.component.scss'
|
||||
})
|
||||
export class ProjectPictureFormComponent {
|
||||
@Input({required: true}) project: Project | undefined = undefined;
|
||||
|
||||
onFormSubmitted = output<any>();
|
||||
private readonly projectService = inject(ProjectService);
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
file: File | null = null; // Variable to store file
|
||||
imagePreviewUrl: string | null = null; // URL for image preview
|
||||
|
||||
onSubmit() {
|
||||
|
||||
if (this.file != null) {
|
||||
const formData = new FormData();
|
||||
formData.append('fichier', this.file); // "fichier" est le nom du champ dans PocketBase
|
||||
|
||||
this.projectService.updateProject(this.project?.id!, formData).subscribe(
|
||||
value => {
|
||||
this.authService.updateUser();
|
||||
|
||||
this.toastrService.success(
|
||||
`L'aperçu du projet ${value.nom} ont bien été modifier !`,
|
||||
`Mise à jour`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
this.onFormSubmitted.emit("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onPictureChange($event: Event) {
|
||||
const target: HTMLInputElement = $event.target as HTMLInputElement;
|
||||
if (target?.files?.[0]) {
|
||||
this.file = target.files[0];
|
||||
this.readFile(this.file);
|
||||
}
|
||||
}
|
||||
|
||||
private readFile(file: File) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imagePreviewUrl = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
protected readonly environment = environment;
|
||||
}
|
||||
106
src/app/shared/components/reseaux/reseaux.component.html
Normal file
106
src/app/shared/components/reseaux/reseaux.component.html
Normal file
@@ -0,0 +1,106 @@
|
||||
@if (reseaux != undefined) {
|
||||
<ul class="flex flex-wrap justify-center mt-4 space-x-2">
|
||||
|
||||
@for (rx of Object.keys(reseaux); track rx) {
|
||||
|
||||
@if (reseaux[rx] != '') {
|
||||
|
||||
<li>
|
||||
<a href="{{reseaux[rx]}}" target="_blank" class="text-[#39569c] hover:text-gray-900 dark:hover:text-white">
|
||||
@switch (rx.toLowerCase()) {
|
||||
@case ("facebook".toLowerCase()) {
|
||||
<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>
|
||||
}
|
||||
@case ("instagram".toLowerCase()) {
|
||||
<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>
|
||||
}
|
||||
@case ("youTube".toLowerCase()) {
|
||||
<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>
|
||||
}
|
||||
@case ("linkedIn".toLowerCase()) {
|
||||
<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>
|
||||
}
|
||||
@case ("github".toLowerCase()) {
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
|
||||
<svg class="w-6 h-6" fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>
|
||||
GitHub</title>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
</svg>
|
||||
|
||||
} @else {
|
||||
|
||||
<svg class="w-6 h-6" fill="#181717" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>
|
||||
GitHub</title>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
</svg>
|
||||
}
|
||||
}
|
||||
@case ("x".toLowerCase()) {
|
||||
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
|
||||
<svg class="w-6 h-6" fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>X</title>
|
||||
<path
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/>
|
||||
</svg>
|
||||
} @else {
|
||||
|
||||
<svg class="w-6 h-6" fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>X</title>
|
||||
<path
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/>
|
||||
</svg>
|
||||
}
|
||||
}
|
||||
@case ("web".toLowerCase()) {
|
||||
|
||||
@if (themeService.darkModeSignal() === 'dark') {
|
||||
|
||||
<svg id="Layer_1" class="w-6 h-6" fill="#FFFFFF" data-name="Layer 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"><title>world-globe-outline</title>
|
||||
<path
|
||||
d="M256,0Q362.11,0,436.9,75.1,512,149.89,512,256T436.9,436.9Q362.11,512,256,512T75.1,436.9Q0,362.11,0,256T75.1,75.1Q149.89,0,256,0ZM55.34,190.63a211.82,211.82,0,0,0,0,130.73h66a416,416,0,0,1,0-130.73Zm14.9,165.7q34.36,63.55,100.64,92.73a232.64,232.64,0,0,1-20.07-31.92,302.59,302.59,0,0,1-22.2-60.81ZM170.87,63.24Q104.59,92.43,70.54,155.37h58.07a304.36,304.36,0,0,1,22.2-60.51A198.45,198.45,0,0,1,170.87,63.24ZM151.72,190.63A390.59,390.59,0,0,0,145.94,256a390.48,390.48,0,0,0,5.78,65.37H241.1V190.63Zm82.7-143.51q-32.83,13.38-57.16,61.11-9.43,19.16-17.63,47.13H241.1V45.61a3.4,3.4,0,0,1-1.52.3h-1.82Zm3.34,419h1.82a5.12,5.12,0,0,0,1.52.3V356.33H159.62q8.21,28.28,17.63,47.43,24.32,47.74,57.16,61.11ZM274.24,45.91h-1.83a3.38,3.38,0,0,1-1.52-.3V155.37h81.48q-8.21-28-17.63-47.13-24.33-47.73-57.16-61.11Zm86,275.46A391.23,391.23,0,0,0,366.06,256a395,395,0,0,0-5.78-65.37H270.9V321.37Zm-82.7,143.51q32.84-13.39,57.16-61.11,9.73-19.16,17.63-47.43H270.9V466.39a5.1,5.1,0,0,0,1.52-.3h1.83ZM441.46,155.37q-34.06-62.94-100-92.12A212.61,212.61,0,0,1,361.2,94.86a295.22,295.22,0,0,1,22.2,60.51Zm-100,293.7q66-29.49,100-92.73H383.39q-8.52,33.74-22.2,60.81A226,226,0,0,1,341.43,449.06Zm49.25-258.43A412,412,0,0,1,395.86,256a415.71,415.71,0,0,1-5.17,65.37h66a211.89,211.89,0,0,0,0-130.73Z"/>
|
||||
</svg>
|
||||
} @else {
|
||||
|
||||
<svg id="Layer_1" class="w-6 h-6" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"><title>world-globe-outline</title>
|
||||
<path
|
||||
d="M256,0Q362.11,0,436.9,75.1,512,149.89,512,256T436.9,436.9Q362.11,512,256,512T75.1,436.9Q0,362.11,0,256T75.1,75.1Q149.89,0,256,0ZM55.34,190.63a211.82,211.82,0,0,0,0,130.73h66a416,416,0,0,1,0-130.73Zm14.9,165.7q34.36,63.55,100.64,92.73a232.64,232.64,0,0,1-20.07-31.92,302.59,302.59,0,0,1-22.2-60.81ZM170.87,63.24Q104.59,92.43,70.54,155.37h58.07a304.36,304.36,0,0,1,22.2-60.51A198.45,198.45,0,0,1,170.87,63.24ZM151.72,190.63A390.59,390.59,0,0,0,145.94,256a390.48,390.48,0,0,0,5.78,65.37H241.1V190.63Zm82.7-143.51q-32.83,13.38-57.16,61.11-9.43,19.16-17.63,47.13H241.1V45.61a3.4,3.4,0,0,1-1.52.3h-1.82Zm3.34,419h1.82a5.12,5.12,0,0,0,1.52.3V356.33H159.62q8.21,28.28,17.63,47.43,24.32,47.74,57.16,61.11ZM274.24,45.91h-1.83a3.38,3.38,0,0,1-1.52-.3V155.37h81.48q-8.21-28-17.63-47.13-24.33-47.73-57.16-61.11Zm86,275.46A391.23,391.23,0,0,0,366.06,256a395,395,0,0,0-5.78-65.37H270.9V321.37Zm-82.7,143.51q32.84-13.39,57.16-61.11,9.73-19.16,17.63-47.43H270.9V466.39a5.1,5.1,0,0,0,1.52-.3h1.83ZM441.46,155.37q-34.06-62.94-100-92.12A212.61,212.61,0,0,1,361.2,94.86a295.22,295.22,0,0,1,22.2,60.51Zm-100,293.7q66-29.49,100-92.73H383.39q-8.52,33.74-22.2,60.81A226,226,0,0,1,341.43,449.06Zm49.25-258.43A412,412,0,0,1,395.86,256a415.71,415.71,0,0,1-5.17,65.37h66a211.89,211.89,0,0,0,0-130.73Z"/>
|
||||
</svg>
|
||||
}
|
||||
}
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
} @empty {
|
||||
<small>Aucune presence sur les réseaux</small>
|
||||
}
|
||||
|
||||
</ul>
|
||||
}
|
||||
23
src/app/shared/components/reseaux/reseaux.component.spec.ts
Normal file
23
src/app/shared/components/reseaux/reseaux.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ReseauxComponent } from './reseaux.component';
|
||||
|
||||
describe('ReseauxComponent', () => {
|
||||
let component: ReseauxComponent;
|
||||
let fixture: ComponentFixture<ReseauxComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReseauxComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReseauxComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
20
src/app/shared/components/reseaux/reseaux.component.ts
Normal file
20
src/app/shared/components/reseaux/reseaux.component.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {Component, inject, Input} from '@angular/core';
|
||||
import {JsonPipe} from "@angular/common";
|
||||
import {ThemeService} from "@app/core/services/theme/theme.service";
|
||||
import {UntilDestroy} from "@ngneat/until-destroy";
|
||||
|
||||
@Component({
|
||||
selector: 'app-reseaux',
|
||||
standalone: true,
|
||||
imports: [
|
||||
JsonPipe
|
||||
],
|
||||
templateUrl: './reseaux.component.html',
|
||||
styleUrl: './reseaux.component.scss'
|
||||
})
|
||||
@UntilDestroy()
|
||||
export class ReseauxComponent {
|
||||
@Input({required: true}) reseaux: any = undefined;
|
||||
protected readonly Object = Object;
|
||||
protected themeService: ThemeService = inject(ThemeService);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
@if (user){
|
||||
|
||||
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white mb-4">Ma photo de profil </h3>
|
||||
|
||||
<div class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400">
|
||||
@if (imagePreviewUrl != null) {
|
||||
<img alt="{{user!.username}}" class="object-cover object-center h-full w-full rounded-full"
|
||||
[src]="imagePreviewUrl" loading="lazy">
|
||||
} @else 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>
|
||||
}
|
||||
|
||||
<label for="uploadFile1"
|
||||
class="flex bg-gray-800 hover:bg-gray-700 text-white text-base px-5 py-3 outline-none rounded w-max cursor-pointer mx-auto font-[sans-serif]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 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 une image</small>
|
||||
<input type="file" id='uploadFile1' class="hidden"
|
||||
accept="image/*"
|
||||
(change)="onPictureChange($event)"/>
|
||||
</label>
|
||||
|
||||
@if (file != null || imagePreviewUrl != null) {
|
||||
<button type="button" [ngClass]="{'bg-purple-600':file!=null || imagePreviewUrl != null}"
|
||||
class="!mt-2 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
||||
(click)="onUserAvatarFormSubmit()">Mettre à jour ma photo de profile
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {UserAvatarFormComponent} from './user-avatar-form.component';
|
||||
import {provideRouter} from "@angular/router";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
import {UserService} from "@app/core/services/user/user.service";
|
||||
|
||||
describe('UserAvatarFormComponent', () => {
|
||||
let component: UserAvatarFormComponent;
|
||||
let fixture: ComponentFixture<UserAvatarFormComponent>;
|
||||
|
||||
let mockToastrService: Partial<ToastrService>;
|
||||
let mockAuthService: Partial<AuthService>;
|
||||
let mockUserService: Partial<UserService>;
|
||||
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
mockToastrService = {
|
||||
warning: jest.fn(),
|
||||
success: jest.fn(),
|
||||
error: jest.fn()
|
||||
};
|
||||
|
||||
mockAuthService = {
|
||||
updateUser: jest.fn()
|
||||
};
|
||||
|
||||
mockUserService = {
|
||||
updateUser: jest.fn().mockReturnValue({
|
||||
subscribe: jest.fn()
|
||||
})
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserAvatarFormComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: ToastrService, useValue: mockToastrService },
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: UserService, useValue: mockUserService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserAvatarFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import {Component, inject, Input, output} from '@angular/core';
|
||||
import {User} from "@app/shared/models/user";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {AuthService} from "@app/core/services/authentication/auth.service";
|
||||
import {UserService} from "@app/core/services/user/user.service";
|
||||
import {environment} from "@env/environment";
|
||||
import {NgClass} from "@angular/common";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-avatar-form',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './user-avatar-form.component.html',
|
||||
styleUrl: './user-avatar-form.component.scss'
|
||||
})
|
||||
export class UserAvatarFormComponent {
|
||||
|
||||
private readonly toastrService = inject(ToastrService);
|
||||
protected readonly environment = environment;
|
||||
@Input({required: true}) user: User | undefined = undefined;
|
||||
|
||||
onFormSubmitted = output<any>();
|
||||
private userService = inject(UserService);
|
||||
|
||||
private authService = inject(AuthService);
|
||||
|
||||
file: File | null = null; // Variable to store file
|
||||
imagePreviewUrl: string | null = null; // URL for image preview
|
||||
|
||||
onUserAvatarFormSubmit() {
|
||||
|
||||
if (this.file != null) {
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', this.file); // "avatar" est le nom du champ dans PocketBase
|
||||
|
||||
this.userService.updateUser(this.user?.id!, formData).subscribe({
|
||||
next: value => {
|
||||
this.authService.updateUser();
|
||||
|
||||
this.toastrService.success(
|
||||
`Votre photo de profile a 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 photo de profile !`,
|
||||
`Erreur`,
|
||||
{
|
||||
closeButton: true,
|
||||
progressAnimation: 'decreasing',
|
||||
progressBar: true
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
this.onFormSubmitted.emit("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onPictureChange($event: Event) {
|
||||
const target: HTMLInputElement = $event.target as HTMLInputElement;
|
||||
if (target?.files?.[0]) {
|
||||
this.file = target.files[0];
|
||||
this.readFile(this.file);
|
||||
}
|
||||
}
|
||||
|
||||
private readFile(file: File) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imagePreviewUrl = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
20
src/app/shared/components/user-form/user-form.component.html
Normal file
20
src/app/shared/components/user-form/user-form.component.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<form class="space-y-6 px-4 max-w-sm mx-auto font-[sans-serif]" [formGroup]="userForm" (ngSubmit)="onUserFormSubmit()">
|
||||
|
||||
<h3 class="font-ubuntu font-bold text-xl uppercase dark:text-white mb-4">Mon Identité </h3>
|
||||
|
||||
<div class="flex items-center">
|
||||
<label class="text-gray-400 w-36 text-xs">Nom</label>
|
||||
<input type="text" placeholder="Enter your name" formControlName="name"
|
||||
class="px-4 py-3 bg-gray-100 focus:bg-transparent w-full text-xs outline-[#333] rounded-sm transition-all"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<label class="text-gray-400 w-36 text-xs">Prénom</label>
|
||||
<input type="text" placeholder="entre votre prénom" formControlName="firstname"
|
||||
class="px-4 py-3 bg-gray-100 focus:bg-transparent w-full text-xs outline-[#333] rounded-sm transition-all"/>
|
||||
</div>
|
||||
|
||||
<button type="submit" [ngClass]="{'bg-purple-600':userForm.valid}"
|
||||
class="!mt-4 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block">Modifier mon identité
|
||||
</button>
|
||||
</form>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user