first commit

This commit is contained in:
2025-07-22 22:10:30 +02:00
commit 9a1ffc51d8
130 changed files with 20161 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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.');
}
}
}

View File

@@ -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'));
};
});
}
}