Adds Local Profile Pictures Implementation

This commit is contained in:
2023-08-27 23:15:06 -03:00
parent f881c16f7f
commit c5d70020b6
11 changed files with 1875 additions and 2418 deletions

4069
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@
"@fortawesome/free-brands-svg-icons": "^6.1.1", "@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1",
"bootstrap": "^4.6.1", "bootstrap": "^4.6.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.1", "express": "^4.18.1",
"jquery": "^3.6.0", "jquery": "^3.6.0",
@@ -41,12 +41,12 @@
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "^14.1.0", "@angular-builders/custom-webpack": "^14.1.0",
"@angular-devkit/build-angular": "^14.0.3", "@angular-devkit/build-angular": "^14.0.3",
"@angular-eslint/builder": "14.0.0", "@angular-eslint/builder": "^14.4.0",
"@angular-eslint/eslint-plugin": "14.0.0", "@angular-eslint/eslint-plugin": "14.0.0",
"@angular-eslint/eslint-plugin-template": "14.0.0", "@angular-eslint/eslint-plugin-template": "14.0.0",
"@angular-eslint/schematics": "14.0.0", "@angular-eslint/schematics": "14.0.0",
"@angular-eslint/template-parser": "14.0.0", "@angular-eslint/template-parser": "14.0.0",
"@angular/cli": "~14.0.3", "@angular/cli": "^14.2.12",
"@angular/compiler-cli": "^14.0.0", "@angular/compiler-cli": "^14.0.0",
"@types/jasmine": "~4.0.0", "@types/jasmine": "~4.0.0",
"@types/node": "^18.11.19", "@types/node": "^18.11.19",

View File

@@ -16,10 +16,9 @@
<div class="row"> <div class="row">
<div class="btn-container"> <div class="btn-container">
<button class="picture-btn" <app-profile-picture-picker
(click)="onAddProfilePicture()"> (imageSent)="onProfilePictureSent($event)">
Add Profile Picture </app-profile-picture-picker>
</button>
</div> </div>
<div class="separator-line"> <div class="separator-line">

View File

@@ -11,6 +11,7 @@ import {first, take} from "rxjs";
import UserChecker from "../../../shared/model/user/user.checker"; import UserChecker from "../../../shared/model/user/user.checker";
import HttpErrorChecker from "../../../shared/model/httpError/httpErrorChecker"; import HttpErrorChecker from "../../../shared/model/httpError/httpErrorChecker";
import {HttpError} from "../../../shared/model/httpError/httpError.model"; import {HttpError} from "../../../shared/model/httpError/httpError.model";
import {faFileUpload} from "@fortawesome/free-solid-svg-icons";
@Component({ @Component({
@@ -101,6 +102,8 @@ export class MyProfileComponent implements OnInit {
isShowErrorMessage = false; isShowErrorMessage = false;
_fileIcon = faFileUpload
constructor(private authService: AuthService) { constructor(private authService: AuthService) {
} }
@@ -116,36 +119,27 @@ export class MyProfileComponent implements OnInit {
this.stateChange.emit(state); this.stateChange.emit(state);
} }
public showErrorMessage(): string { showErrorMessage(): string {
if (this.isShowErrorMessage) { if (this.isShowErrorMessage) {
return "show"; return "show";
} }
return "hide"; return "hide";
} }
public hideErrorMessage(): string { hideErrorMessage(): string {
if (!!this.errorMessage) { if (!!this.errorMessage) {
return "hide"; return "hide";
} }
return "show"; return "show";
} }
public onDeleteAccount() { onDeleteAccount() {
this.authService.deleteAccount().subscribe({ this.authService.deleteAccount().subscribe({
next: (res) => { next: (response: any) => {
if (res && UserChecker.test(res)) { this.authService.logout();
this.closePopup()
} if (HttpErrorChecker.test(res)) {
this.errorMessage = (<HttpError>res).details;
}
} }
}) })
// this.authService.logout() this.closePopup();
// this.onStateChange(false);
}
public onAddProfilePicture() {
this.authService.addProfilePicture()
} }
hideAuthContainer(event: any) { hideAuthContainer(event: any) {
@@ -155,6 +149,12 @@ export class MyProfileComponent implements OnInit {
} }
} }
onProfilePictureSent(event: any) {
if (event) {
this.closePopup();
}
}
private closePopup() { private closePopup() {
this.onStateChange(false); this.onStateChange(false);
} }

View File

@@ -0,0 +1,20 @@
<div class="btn-container">
<div class="input-group mb-3">
<div class="custom-file">
<input type="file"
class="custom-file-input"
id="inputProfilePicture"
aria-describedby="inputProfilePicture"
(change)="handleFileInput($event)">
<label class="custom-file-label"
for="inputProfilePicture">
Profile Picture
</label>
</div>
<div class="input-group-append">
<button class="btn btn-outline-secondary"
[disabled]="isProfilePictureSelected"
(click)="uploadProfilePicture()">Upload</button>
</div>
</div>
</div>

View File

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

View File

@@ -0,0 +1,35 @@
import {Component, EventEmitter, Output} from '@angular/core';
import {AuthService} from "../../../../shared/auth/auth.service";
@Component({
selector: 'app-profile-picture-picker',
templateUrl: './profile-picture-picker.component.html',
styleUrls: ['./profile-picture-picker.component.css']
})
export class ProfilePicturePickerComponent {
@Output()
imageSent = new EventEmitter<boolean>();
private profilePicture!: File;
constructor(private authService: AuthService) { }
handleFileInput(event: Event) {
const element = event.currentTarget as HTMLInputElement;
const fileList: FileList | null = element.files;
if (fileList != null && fileList.length > 0 && fileList[0] != null) {
this.profilePicture = fileList[0];
}
}
uploadProfilePicture() {
this.authService.addProfilePicture(this.profilePicture);
this.imageSent.emit(true);
}
get isProfilePictureSelected(): boolean {
return !this.profilePicture;
}
}

View File

@@ -18,6 +18,7 @@ import {MatIconModule} from '@angular/material/icon';
import { ErrorBoxComponent } from './header-popup/error-box/error-box.component'; import { ErrorBoxComponent } from './header-popup/error-box/error-box.component';
import { HelpComponent } from './header-popup/help/help.component'; import { HelpComponent } from './header-popup/help/help.component';
import { MyProfileComponent } from './header-popup/my-profile/my-profile.component'; import { MyProfileComponent } from './header-popup/my-profile/my-profile.component';
import { ProfilePicturePickerComponent } from './header-popup/my-profile/profile-picture-picker/profile-picture-picker.component';
@@ -34,6 +35,7 @@ import { MyProfileComponent } from './header-popup/my-profile/my-profile.compone
ErrorBoxComponent, ErrorBoxComponent,
HelpComponent, HelpComponent,
MyProfileComponent, MyProfileComponent,
ProfilePicturePickerComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@@ -1,11 +1,11 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {first, firstValueFrom, Observable, of, Subject, take} from 'rxjs'; import {first, firstValueFrom, map, Observable, of, Subject, take, tap} from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { HttpError } from '../model/httpError/httpError.model'; import { HttpError } from '../model/httpError/httpError.model';
import { Token } from '../model/token/token.model';
import { User } from '../model/user/user.model'; import { User } from '../model/user/user.model';
import * as http from "http";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -56,14 +56,28 @@ export class AuthService {
} }
deleteAccount() { deleteAccount() {
return this.deleteAccountRequest().pipe( return this.deleteAccountRequest();
first()
);
} }
addProfilePicture() { addProfilePicture(file: File): void {
const fileType = file.type.split('/')[1];
this.getAddProfilePictureUrl(fileType).subscribe({
next: (url: string|null) => {
if (url != null) {
this.uploadProfilePicture(url, file).then(
(response: Observable<any>) => {
response.subscribe({
next: (response: any) => {
this.processProfilePicture().subscribe();
}
});
}
);
}
}
})
} }
private loginUser(userAuthAtempt: User): Observable<User|any> { private loginUser(userAuthAtempt: User): Observable<User|any> {
let loginParams = new URLSearchParams(); let loginParams = new URLSearchParams();
@@ -154,9 +168,11 @@ export class AuthService {
} }
private deleteAccountRequest() { private deleteAccountRequest() {
let headers = this.createAuthorizationHeader()
return this.http.delete( return this.http.delete(
this.BACKEND_PATH + `/user/delete/${this.userAuthenticated.id}`, this.BACKEND_PATH + `/user/delete`,
{ withCredentials: true } { headers: headers, withCredentials: true }
); );
} }
@@ -182,4 +198,68 @@ export class AuthService {
} }
}); });
} }
private getAddProfilePictureUrl(fileType: string): Observable<string|null> {
return this.http.post<{ presigned_url: string, file_key: string }>(
this.BACKEND_PATH + '/user/profile-picture?fileType=' + fileType,
null,
{
headers: this.createAuthorizationHeader(),
withCredentials: true
}
).pipe(
first(),
map((res) => {
if (!!res && !!res.presigned_url) {
return res.presigned_url;
}
return null
})
)
}
private async uploadProfilePicture(url: string, file: File): Promise<Observable<any>> {
const fileData = await this.readAsArrayBuffer(file);
let headers = new HttpHeaders({
'Content-Type': file.type
})
return this.http.put(
url,
fileData,
{
headers: headers,
}
);
}
private processProfilePicture() {
return this.http.post(
this.BACKEND_PATH + '/user/profile-picture/proccess',
null,
{
headers: this.createAuthorizationHeader(),
withCredentials: true
}
)
}
private createAuthorizationHeader(): HttpHeaders {
return new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.userAuthenticated.accessToken?.token
});
}
private async readAsArrayBuffer(file: File): Promise<ArrayBuffer> {
const reader = new FileReader();
reader.readAsArrayBuffer(file)
return new Promise<ArrayBuffer>((resolve, reject) => {
reader.onload = () => {
resolve(reader.result as ArrayBuffer);
};
reader.onerror = () => {
reject(reader.error);
};
});
}
} }

View File

@@ -1,5 +1,4 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
@import 'font-montserrat.css'; @import 'font-montserrat.css';
* { * {
@@ -9,4 +8,4 @@
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: 'Montserrat', sans-serif; font-family: 'Montserrat', sans-serif;
} }