first commit
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
.upload-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-container button {
|
||||
background-color: rgba(0, 0, 0, 0.25); ;
|
||||
color: #FFF;
|
||||
border-radius: 50%;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-right: 5px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.profile-photo-upload-container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="profile-photo-upload-container" style="margin-bottom: 40px;">
|
||||
<!-- Hidden file input for styling purposes -->
|
||||
<input type="file" #fileInput hidden (change)="onFileSelected($event)" accept="image/*">
|
||||
<div class="upload-container">
|
||||
<button *ngIf="!currentUser?.photoUrl" (click)="fileSelected ? uploadFile() : fileInput.click()" class="image-upload-button">
|
||||
<mat-icon>{{ fileSelected ? 'upload_file' : 'image' }}</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="currentUser?.photoUrl" mat-fab (click)="deleteFile()" class="close-icon-button">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProfilePhotoUploadComponent } from './profile-photo-upload.component';
|
||||
|
||||
describe('ProfilePhotoUploadComponent', () => {
|
||||
let component: ProfilePhotoUploadComponent;
|
||||
let fixture: ComponentFixture<ProfilePhotoUploadComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProfilePhotoUploadComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProfilePhotoUploadComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { UserService } from '../../user.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { User } from '../../../auth/model/user.model';
|
||||
import { ProfilePhotoUploadService } from './profile-photo-upload.service';
|
||||
import { AngularMaterialModule } from '../../../angular-material/angular-material.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NotifierService } from '../../../shared/notifierService/notifier.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile-photo-upload',
|
||||
standalone: true,
|
||||
imports: [AngularMaterialModule, CommonModule],
|
||||
templateUrl: './profile-photo-upload.component.html',
|
||||
styleUrls: ['./profile-photo-upload.component.css'],
|
||||
})
|
||||
export class ProfilePhotoUploadComponent implements OnInit, OnDestroy {
|
||||
//TODO check why the userId remains empty/null in component, but in service the uid is returned proper?
|
||||
userProfileSubscription!: Subscription;
|
||||
currentUser: User | null = null;
|
||||
fileSelected: boolean = false;
|
||||
metadata = {
|
||||
contentType: 'image/jpeg',
|
||||
customMetadata: {
|
||||
imgId: undefined,
|
||||
},
|
||||
};
|
||||
selectedFile: File | null = null;
|
||||
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private profilePhotoUploadService: ProfilePhotoUploadService,
|
||||
private notifierService: NotifierService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userProfileSubscription =
|
||||
this.userService.currentUserProfile$.subscribe((user: User | null) => {
|
||||
this.currentUser = user;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.userProfileSubscription) {
|
||||
this.userProfileSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
onFileSelected(event: any) {
|
||||
this.selectedFile = event.target.files[0];
|
||||
this.fileSelected = true;
|
||||
this.notifierService.showNotification(
|
||||
`Datei wurde ausgewählt. Bitte klicke auf die Schaltfläche "Hochladen", um Bild einzustellen.`,
|
||||
'OK'
|
||||
);
|
||||
}
|
||||
|
||||
async uploadFile() {
|
||||
if (this.selectedFile && this.currentUser && this.currentUser.uid) {
|
||||
const file = this.selectedFile;
|
||||
const metadata = {
|
||||
contentType: 'image/jpeg',
|
||||
customMetadata: {
|
||||
imgId: this.currentUser?.uid,
|
||||
},
|
||||
};
|
||||
await this.profilePhotoUploadService.uploadFile(
|
||||
this.currentUser.uid,
|
||||
file,
|
||||
metadata
|
||||
);
|
||||
this.notifierService.showNotification(`Datei wurde hochgeladen`, 'OK');
|
||||
} else {
|
||||
this.notifierService.showInformation(
|
||||
'Keine Datei ausgewählt oder Benutzer-ID ist unbekannt.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile() {
|
||||
if (this.currentUser && this.currentUser.uid) {
|
||||
await this.profilePhotoUploadService.deleteFile(this.currentUser.uid);
|
||||
this.notifierService.showNotification(`Datei wurde gelöscht`, 'OK');
|
||||
} else {
|
||||
this.notifierService.showInformation('Benutzer-ID ist unbekannt. Bitte melde dich an, um die Datei zu löschen.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
getStorage,
|
||||
ref,
|
||||
uploadBytesResumable,
|
||||
getDownloadURL,
|
||||
deleteObject,
|
||||
} from 'firebase/storage';
|
||||
import {
|
||||
getFirestore,
|
||||
doc,
|
||||
setDoc,
|
||||
getDoc,
|
||||
deleteDoc,
|
||||
updateDoc,
|
||||
} from 'firebase/firestore';
|
||||
import { NotifierService } from '../../../shared/notifierService/notifier.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProfilePhotoUploadService {
|
||||
storage = getStorage();
|
||||
firestore = getFirestore();
|
||||
|
||||
constructor(private notifierService: NotifierService) {}
|
||||
|
||||
async uploadFile(userId: string, file: File, metadata: any): Promise<void> {
|
||||
// get the current user's file metadata from firestore
|
||||
const userDocRef = doc(this.firestore, 'users', userId);
|
||||
const fileDocRef = doc(this.firestore, 'files', userId);
|
||||
const fileDoc = await getDoc(fileDocRef);
|
||||
|
||||
if (fileDoc.exists()) {
|
||||
// if there's an existing file, delete it from storage
|
||||
const existingFilePath = fileDoc.data()['filePath'];
|
||||
const existingFileRef = ref(this.storage, existingFilePath);
|
||||
await deleteObject(existingFileRef).catch((error) => {
|
||||
(`Error deleting existing file: ${{error}}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (file.size > 30 * 1024) {
|
||||
// If the file is too large, compress the image
|
||||
this.notifierService.showInformation('File size exceeds 30 KB. Compressing the image...')
|
||||
file = await this.compressImage(file);
|
||||
}
|
||||
|
||||
//creates a firebase storage for images
|
||||
const filePath = `images/${userId}/${file.name}`;
|
||||
const storageRef = ref(this.storage, filePath);
|
||||
|
||||
//uploads the image to the storage
|
||||
const uploadTask = uploadBytesResumable(storageRef, file, metadata);
|
||||
uploadTask.on(
|
||||
'state_changed',
|
||||
//show the upload process in the console
|
||||
(snapshot) => {
|
||||
switch (snapshot.state) {
|
||||
case 'error':
|
||||
this.notifierService.showInformation('Upload failed')
|
||||
break;
|
||||
case 'success':
|
||||
this.notifierService.showInformation('Upload completed')
|
||||
break;
|
||||
}
|
||||
},
|
||||
//error handling, display currently only in the console
|
||||
(error) => {
|
||||
switch (error.code) {
|
||||
case 'storage/unauthorized':
|
||||
this.notifierService.showInformation('The user is not authenticated. Please authenticate yourself and try again.');
|
||||
break;
|
||||
case 'storage/canceled':
|
||||
this.notifierService.showInformation('The user has cancelled the process.');
|
||||
break;
|
||||
case 'storage/unknown':
|
||||
this.notifierService.showInformation('An unknown error has occurred.');
|
||||
break;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
//returns the download url of the uploaded image
|
||||
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
||||
const fileMetadata = {
|
||||
id: userId,
|
||||
filePath: filePath,
|
||||
downloadURL: downloadURL,
|
||||
};
|
||||
// Set the file metadata in the 'files' collection
|
||||
setDoc(fileDocRef, fileMetadata).catch((error) => {
|
||||
this.notifierService.showInformation(`Error updating file${{error}}`);
|
||||
});
|
||||
// Update the user's document with the new photoUrl
|
||||
updateDoc(userDocRef, { photoUrl: downloadURL })
|
||||
.catch((error) => {
|
||||
this.notifierService.showInformation(`Error updating user document with photo URL: ${{error}}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async deleteFile(userId: string): Promise<void> {
|
||||
try {
|
||||
// Get the current user's file metadata from Firestore
|
||||
const userDocRef = doc(this.firestore, 'users', userId);
|
||||
const fileDocRef = doc(this.firestore, 'files', userId);
|
||||
const fileDoc = await getDoc(fileDocRef);
|
||||
if (fileDoc.exists()) {
|
||||
// If there's an existing file, delete it from storage
|
||||
const existingFilePath = fileDoc.data()['filePath'];
|
||||
const existingFileRef = ref(this.storage, existingFilePath);
|
||||
await deleteObject(existingFileRef);
|
||||
await deleteDoc(fileDocRef);
|
||||
updateDoc(userDocRef, { photoUrl: null })
|
||||
.catch((error) => {
|
||||
this.notifierService.showInformation(`Error removing photo URL from user document: ${{error}}`);
|
||||
});
|
||||
} else {
|
||||
this.notifierService.showInformation('No file metadata found to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
this.notifierService.showInformation(`Error deleting file: ${{error}}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compress an image
|
||||
private compressImage(file: File): Promise<File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(file);
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
return;
|
||||
}
|
||||
const maxWidth = 1024; // Set the maximum width you want for the images
|
||||
const maxHeight = 1024; // Set the maximum height you want for the images
|
||||
let width = image.width;
|
||||
let height = image.height;
|
||||
|
||||
if (width > height) {
|
||||
if (width > maxWidth) {
|
||||
height *= maxWidth / width;
|
||||
width = maxWidth;
|
||||
}
|
||||
} else {
|
||||
if (height > maxHeight) {
|
||||
width *= maxHeight / height;
|
||||
height = maxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(image, 0, 0, width, height);
|
||||
|
||||
// Function to attempt to compress the image
|
||||
const attemptCompression = (quality: number, resolve: (file: File) => void, reject: (reason: Error) => void) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob === null) {
|
||||
reject(new Error('Failed to create blob from canvas'));
|
||||
return;
|
||||
}
|
||||
if (blob.size <= 30 * 1024) {
|
||||
// If the blob size is less than or equal to 100 KB, resolve the promise
|
||||
const compressedFile = new File([blob], file.name, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
resolve(compressedFile);
|
||||
} else if (quality > 0.1) {
|
||||
// If the quality can still be reduced, try again with lower quality
|
||||
attemptCompression(quality - 0.1, resolve, reject);
|
||||
} else {
|
||||
// If the quality is already too low, reject the promise
|
||||
reject(new Error('Cannot compress image to the desired size'));
|
||||
}
|
||||
},
|
||||
'image/jpeg',
|
||||
quality
|
||||
);
|
||||
};
|
||||
|
||||
// Start the compression attempt with an initial quality value
|
||||
attemptCompression(0.5, resolve, reject);
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
reject(new Error('Image loading failed'));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user