update du nom d'utilisateur ainsi que de la photo de profil

This commit is contained in:
Guams 2025-01-22 20:50:53 +01:00
parent b1b5995ea8
commit 2017b16d4c
15 changed files with 321 additions and 44 deletions

View File

@ -46,7 +46,7 @@ export class HeaderComponent {
if (author.role === 'WRITER') { if (author.role === 'WRITER') {
return [ return [
...commonItems, ...commonItems,
this.getWriterMenu(author), this.getWriterMenu(),
this.getUserMenu(author) this.getUserMenu(author)
]; ];
} else if (author.role === 'ADMIN') { } else if (author.role === 'ADMIN') {
@ -65,7 +65,7 @@ export class HeaderComponent {
return commonItems; return commonItems;
} }
private getWriterMenu(author: Author): MenuItem { private getWriterMenu(): MenuItem {
return { return {
label: 'Mes posts', label: 'Mes posts',
icon: 'pi pi-file-edit', icon: 'pi pi-file-edit',
@ -97,7 +97,7 @@ export class HeaderComponent {
private getUserMenu(author: Author): MenuItem { private getUserMenu(author: Author): MenuItem {
return { return {
label: author.name.toUpperCase(), label: 'MOI',
icon: 'pi pi-user', icon: 'pi pi-user',
items: [ items: [
{ label: 'Voir mon profil', icon: 'pi pi-user', routerLink: [`/profile`, `${author.id}`] }, { label: 'Voir mon profil', icon: 'pi pi-user', routerLink: [`/profile`, `${author.id}`] },

View File

@ -16,3 +16,23 @@ form {
.send-button { .send-button {
align-self: center; align-self: center;
} }
.add-file-button {
margin: 0;
font-size: 40px;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
gap: 1rem;
}
.pi-plus {
font-size: 40px;
}
.add-file-button :hover {
cursor: pointer;
}

View File

@ -17,7 +17,14 @@
[showCancelButton]="false" [showCancelButton]="false"
chooseLabel="Sélectionner une image" chooseLabel="Sélectionner une image"
(onSelect)="onSelected($event)"> (onSelect)="onSelected($event)">
<ng-template pTemplate="content"></ng-template> <ng-template pTemplate="header"></ng-template>
<ng-template let-chooseCallback="chooseCallback" pTemplate="content" let-files>
@if (!files?.length) {
<div (click)="choose(chooseCallback)" class="add-file-button">
<i class="pi pi-plus"></i><span>Ajouter un fichier</span>
</div>
}
</ng-template>
</p-fileUpload> </p-fileUpload>
<label>Contenu du post</label> <label>Contenu du post</label>

View File

@ -11,6 +11,7 @@ import { EditorModule } from 'primeng/editor';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import {Author} from '../../models/author'; import {Author} from '../../models/author';
import {AuthorService} from '../../services/author.service'; import {AuthorService} from '../../services/author.service';
import {JsonPipe} from '@angular/common';
@Component({ @Component({
selector: 'app-post-form', selector: 'app-post-form',
@ -20,7 +21,8 @@ import {AuthorService} from '../../services/author.service';
InputTextModule, InputTextModule,
InputTextareaModule, InputTextareaModule,
FileUploadModule, FileUploadModule,
EditorModule EditorModule,
JsonPipe
], ],
templateUrl: './post-form.component.html', templateUrl: './post-form.component.html',
styleUrls: ['./post-form.component.css'] styleUrls: ['./post-form.component.css']
@ -91,13 +93,6 @@ export class PostFormComponent implements OnDestroy {
} }
} }
// transformParagraphs(): void {
// if (this.body) {
// this.body = this.body.replace(/&nbsp;/g, ' ');
// console.log(this.body);
// }
// }
onSubmit(): void { onSubmit(): void {
if (this.form.valid && this.uploadedFile) { if (this.form.valid && this.uploadedFile) {
const formData = this.form.value; const formData = this.form.value;
@ -143,6 +138,10 @@ export class PostFormComponent implements OnDestroy {
} }
} }
choose(callback: Function) {
callback();
}
successMessage(summary: string, detail: string): void { successMessage(summary: string, detail: string): void {
this.messageService.add({ this.messageService.add({
severity: 'success', severity: 'success',

View File

@ -0,0 +1,38 @@
.form-container {
margin-top: 2em;
display: flex;
justify-content: center;
align-items: center;
}
.form {
width: 100%;
max-width: 50em;
display: flex;
flex-direction: column;
gap:1em;
}
.send-button {
align-self: center;
}
.add-file-button {
margin: 0;
font-size: 25px;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
gap: 1rem;
}
.pi-plus {
font-size: 25px;
}
.add-file-button :hover {
cursor: pointer;
}

View File

@ -0,0 +1,28 @@
<div class="form-container">
<form class="form" [formGroup]="form" (ngSubmit)="onSubmit()">
<label for="username">Nom d'utilisateur</label>
<input [(ngModel)]="username" id="username" type="text" pInputText formControlName="username"/>
<label for="password">Mot de passe</label>
<input type="password" [(ngModel)]="password" id="password" pInputText formControlName="password"/>
<label for="passwordConfirm">Confirmez le mot de passe</label>
<input type="password" [(ngModel)]="passwordConfirm" id="passwordConfirm" pInputText
formControlName="passwordConfirm"/>
<p-fileUpload
accept="image/*"
maxFileSize="1000000"
[showUploadButton]="false"
[showCancelButton]="false"
chooseLabel="Sélectionner une image"
(onSelect)="onSelected($event)">
<ng-template pTemplate="header"></ng-template>
<ng-template let-chooseCallback="chooseCallback" pTemplate="content" let-files>
@if (!files?.length) {
<div (click)="choose(chooseCallback)" class="add-file-button">
<i class="pi pi-plus"></i><span>Ajouter un fichier</span>
</div>
}
</ng-template>
</p-fileUpload>
<p-button class="send-button" type="submit" label="Modifier"></p-button>
</form>
</div>

View File

@ -0,0 +1,121 @@
import {Component, EventEmitter, Input, OnDestroy, Output} from '@angular/core';
import {Button} from 'primeng/button';
import {InputTextModule} from 'primeng/inputtext';
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {SelectButtonModule} from 'primeng/selectbutton';
import {Subscription, switchMap} from 'rxjs';
import {AuthorService} from '../../services/author.service';
import {MessageService} from 'primeng/api';
import {FileSelectEvent, FileUploadModule} from 'primeng/fileupload';
import {CookieService} from 'ngx-cookie-service';
import {Author} from '../../models/author';
import {Router} from '@angular/router';
@Component({
selector: 'app-update-profile',
standalone: true,
imports: [
Button,
InputTextModule,
ReactiveFormsModule,
SelectButtonModule,
FileUploadModule
],
templateUrl: './update-profile-form.component.html',
styleUrl: './update-profile-form.component.css'
})
export class UpdateProfileFormComponent implements OnDestroy {
@Input() username: string = "";
@Input() authorId: string = "";
password: string = "";
passwordConfirm: string = "";
@Output() updatedAuthorEvent: EventEmitter<Author> = new EventEmitter();
uploadedFile: File | undefined;
subs: Subscription[] = [];
form: FormGroup;
constructor(private formBuilder: FormBuilder,
private authorService: AuthorService,
private messageService: MessageService,
private cookieService: CookieService,
private router: Router,
) {
this.form = this.formBuilder.group({
username: ['', [Validators.required, Validators.maxLength(255)]],
password: ['', [Validators.required, Validators.maxLength(50)]],
passwordConfirm: ['', [Validators.required, Validators.maxLength(50)]],
});
}
onSelected(event: FileSelectEvent): void {
if (event.currentFiles && event.currentFiles.length > 0) {
this.uploadedFile = event.currentFiles[0];
}
}
choose(callback: Function) {
callback();
}
successMessage(summary: string, detail: string): void {
this.messageService.add({
severity: 'success',
summary: summary,
detail: detail,
life: 3000,
closable: false
});
}
failureMessage(summary: string, detail: string): void {
this.messageService.add({
severity: 'error',
summary: summary,
detail: detail,
life: 3000,
closable: false
});
}
onSubmit() {
const token: string | undefined = this.cookieService.get('token');
if (this.form.valid && token && this.password === this.passwordConfirm) {
const newUsername = this.form.value.username;
if (this.uploadedFile) {
this.subs.push(this.authorService.updateAuthor(this.authorId, token, newUsername, this.password).pipe(
switchMap((_) => {
return this.authorService.changeAvatar(this.authorId, this.uploadedFile, token)
})
).subscribe({
next: (author: Author) => {
this.successMessage("Mise à jour réussie", "Profil mit à jour avec succès");
this.updatedAuthorEvent.emit(author);
this.cookieService.set('author', JSON.stringify(author));
this.router.navigate(['/']);
},
error: (err) => {
this.failureMessage("Erreur", err.message);
}
}));
} else {
this.subs.push(this.authorService.updateAuthor(this.authorId, token, newUsername, this.password).subscribe({
next: (author: Author) => {
this.successMessage("Mise à jour réussie", "Profil mit à jour avec succès");
this.updatedAuthorEvent.emit(author);
this.cookieService.set('author', JSON.stringify(author));
this.router.navigate(['/']);
},
error: (err) => {
this.failureMessage("Erreur", err.message);
}
}));
}
}
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe())
}
}

View File

@ -1,6 +1,6 @@
export interface Author { export interface Author {
id: string id: string
name: string name: string
avatar: string profilePicture: string
role: string role: string
} }

View File

@ -1,5 +1,7 @@
.main-content { @media only screen and (min-width: 1080px) {
margin: auto; .main-content {
width: 45%; margin: auto;
padding: 2rem; width: 45%;
padding: 2rem;
}
} }

View File

@ -47,8 +47,8 @@ export class LoginComponent implements OnDestroy {
})) }))
.subscribe({ .subscribe({
next: (author: Author) => { next: (author: Author) => {
console.log(author)
this.cookieService.set("author", JSON.stringify(author)); this.cookieService.set("author", JSON.stringify(author));
this.cookieService.set("just-authenticated", "true");
this.getAuthorCookie(); this.getAuthorCookie();
this.router.navigate(['/']).then(() => { this.router.navigate(['/']).then(() => {
this.successMessage('Connecté avec succès', 'Heureux de vous revoir ' + this.actualAuthor?.name) this.successMessage('Connecté avec succès', 'Heureux de vous revoir ' + this.actualAuthor?.name)
@ -57,7 +57,6 @@ export class LoginComponent implements OnDestroy {
error: (err) => this.failureMessage('Erreur de connexion', err.message) error: (err) => this.failureMessage('Erreur de connexion', err.message)
}) })
); );
} else { } else {
this.failureMessage('Erreur de connexion', 'Les deux mots de passe ne correspondent pas') this.failureMessage('Erreur de connexion', 'Les deux mots de passe ne correspondent pas')
} }

View File

@ -19,7 +19,10 @@ export class LogoutComponent implements OnInit{
private messageService: MessageService, private messageService: MessageService,
private router: Router) { } private router: Router) { }
ngOnInit(): void { ngOnInit(): void {
this.cookiesService.deleteAll("/", "localhost"); // this.cookiesService.deleteAll("/", "localhost"); // deleteAll est bugué
this.cookiesService.delete('author')
this.cookiesService.delete('just-authenticated')
this.cookiesService.delete('token')
this.router.navigate(['/']).then(() => this.successMessage('Déconnexion', 'Vous avez été deconnecté avec succès')); this.router.navigate(['/']).then(() => this.successMessage('Déconnexion', 'Vous avez été deconnecté avec succès'));
} }

View File

@ -1,10 +1,7 @@
.content { .content {
margin-top: 5rem; margin-top: 5rem;
width: 45%;
display: flex; display: flex;
align-items: center; justify-content: center;
flex-direction: row;
justify-content: space-between;
gap: 3em; gap: 3em;
} }
@ -17,7 +14,17 @@ em {
margin: 0; margin: 0;
} }
.user-info { .user-infos {
margin-bottom: 4rem;
display: flex;
align-self: stretch;
justify-content: space-between;
flex-direction: row;
}
.panel {
gap: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
} }

View File

@ -1,21 +1,39 @@
<app-header></app-header> <app-header></app-header>
<div class="content"> @if (concernedAuthor) {
<div class="user-info"> <div class="content">
<h2>{{ authorName }}</h2> <div class="panel">
<em>{{ concernedAuthor?.role }}</em> <div class="user-infos">
</div> <div>
<div class="profile-picture"> <h2>{{ authorName }}</h2>
@if (concernedAuthor?.avatar) { <em>{{ concernedAuthor.role }}</em>
<p-avatar image="data:image/jpeg;base64,{{ concernedAuthor?.avatar }}" styleClass="mr-2" size="xlarge"></p-avatar> </div>
} @else { <div class="profile-picture">
<p-avatar label="{{ authorName.charAt(0).toUpperCase() }}" styleClass="mr-2" size="xlarge"></p-avatar> @if (concernedAuthor.profilePicture) {
} <p-avatar image="data:image/jpeg;base64,{{ concernedAuthor.profilePicture }}" shape="circle" styleClass="mr-2"
</div> size="xlarge"></p-avatar>
} @else {
<p-avatar label="{{ authorName.charAt(0).toUpperCase() }}" styleClass="mr-2" size="xlarge"></p-avatar>
}
</div>
</div>
<p-button label="Voir les posts de l'utilisateur"></p-button>
@if (actualAuthor) {
@if (concernedAuthor.id === actualAuthor.id) {
<p-button label="Mettre à jour les données du profil" (onClick)="updateProfileDialog=true"/>
<p-dialog header='Mettre à jour le profil' [modal]="true" [(visible)]="updateProfileDialog">
<app-update-profile (updatedAuthorEvent)="updateAuthor($event)" [authorId]="concernedAuthor.id"
[username]="concernedAuthor!.name"></app-update-profile>
</p-dialog>
<p-button label="Changer le mot de passe" (onClick)="changePasswordDialog=true"/>
<p-dialog header='Changer le mot de passe' [modal]="true"
[(visible)]="changePasswordDialog">
<!-- <p-button label="Voir les posts de l'utilisateur"></p-button>--> </p-dialog>
<!-- @if (concernedAuthor?.id === actualAuthor?.id) {--> }
<!-- <p-button label="Mettre à jour les données du profil"/>--> }
<!-- <p-button label="Changer le mot de passe"/>--> </div>
<!-- }--> </div>
</div> } @else {
<h1>Loading...</h1>
}

View File

@ -8,6 +8,8 @@ import {AuthorService} from '../../services/author.service';
import {AvatarModule} from 'primeng/avatar'; import {AvatarModule} from 'primeng/avatar';
import {CardModule} from 'primeng/card'; import {CardModule} from 'primeng/card';
import {Button} from 'primeng/button'; import {Button} from 'primeng/button';
import {DialogModule} from 'primeng/dialog';
import {UpdateProfileFormComponent} from '../../components/update-profile-form/update-profile-form.component';
@Component({ @Component({
selector: 'app-profile', selector: 'app-profile',
@ -16,7 +18,9 @@ import {Button} from 'primeng/button';
HeaderComponent, HeaderComponent,
AvatarModule, AvatarModule,
CardModule, CardModule,
Button Button,
DialogModule,
UpdateProfileFormComponent
], ],
templateUrl: './profile.component.html', templateUrl: './profile.component.html',
styleUrl: './profile.component.css' styleUrl: './profile.component.css'
@ -26,6 +30,8 @@ export class ProfileComponent implements OnDestroy {
concernedAuthor: Author | undefined; concernedAuthor: Author | undefined;
authorName: string = ""; authorName: string = "";
subs: Subscription[] = []; subs: Subscription[] = [];
updateProfileDialog: boolean = false;
changePasswordDialog: boolean = false;
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private authorService: AuthorService, private authorService: AuthorService,
@ -41,6 +47,11 @@ export class ProfileComponent implements OnDestroy {
} }
} }
updateAuthor(author: Author) {
this.concernedAuthor = author;
this.actualAuthor = author;
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe()); this.subs.forEach(sub => sub.unsubscribe());
} }

View File

@ -34,6 +34,30 @@ export class AuthorService {
return this.httpClient.put<any>(`${this.url}/${authorId}/posts`, postId, httpOptions); return this.httpClient.put<any>(`${this.url}/${authorId}/posts`, postId, httpOptions);
} }
updateAuthor(authorId: string, token: string, username: string, password: string): Observable<Author> {
const httpOptions = {
headers: new HttpHeaders({
'Authorization': `Bearer ${token}`
})
}
return this.httpClient.put<Author>(`${this.url}/${authorId}`, {name: username, password: password}, httpOptions);
}
changeAvatar(id: string | undefined, image: File | undefined, token: string) {
if (image) {
const formData: FormData = new FormData();
formData.append('avatar', image);
const httpOptions = {
headers: new HttpHeaders({
'Authorization': `Bearer ${token}`
})
};
return this.httpClient.put<Author>(`${this.url}/${id}/avatar`, formData, httpOptions);
} else {
throw new Error('Image doesn\'t exist');
}
}
getAuthor(id: string | null): Observable<Author> { getAuthor(id: string | null): Observable<Author> {
if (id) { if (id) {
return this.httpClient.get<Author>(`${this.url}/${id}`); return this.httpClient.get<Author>(`${this.url}/${id}`);