From 89bb85a512d4895dd001d80ef29ad33808f2e662 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 29 Mar 2024 20:59:34 +0100 Subject: [PATCH] image-cropper component, drag & drop bilder --- bizmatch-server/src/file/file.service.ts | 13 +- bizmatch-server/src/image/image.controller.ts | 30 ++- bizmatch-server/src/image/image.module.ts | 3 +- .../select-options/select-options.service.ts | 7 +- bizmatch/package.json | 1 + .../image-cropper.component.html | 15 ++ .../image-cropper.component.scss | 0 .../image-cropper/image-cropper.component.ts | 64 ++++++ .../details-listing.component.html | 2 +- .../details-listing.component.ts | 6 +- .../account/account.component.html | 37 +-- .../subscription/account/account.component.ts | 175 +++++++-------- .../edit-listing/edit-listing.component.html | 34 +-- .../edit-listing/edit-listing.component.scss | 93 +++++++- .../edit-listing/edit-listing.component.ts | 212 +++++++++++------- bizmatch/src/app/pages/utils/defaults.ts | 9 + bizmatch/src/app/services/image.service.ts | 23 +- bizmatch/src/app/services/loading.service.ts | 5 +- bizmatch/src/styles.scss | 5 + common-models/src/main.model.ts | 5 +- 20 files changed, 478 insertions(+), 261 deletions(-) create mode 100644 bizmatch/src/app/components/image-cropper/image-cropper.component.html create mode 100644 bizmatch/src/app/components/image-cropper/image-cropper.component.scss create mode 100644 bizmatch/src/app/components/image-cropper/image-cropper.component.ts create mode 100644 bizmatch/src/app/pages/utils/defaults.ts diff --git a/bizmatch-server/src/file/file.service.ts b/bizmatch-server/src/file/file.service.ts index 4d18558..11d9b16 100644 --- a/bizmatch-server/src/file/file.service.ts +++ b/bizmatch-server/src/file/file.service.ts @@ -30,8 +30,6 @@ export class FileService { return this.subscriptions } async storeProfilePicture(file: Express.Multer.File, userId: string) { - // const suffix = file.mimetype.includes('png') ? 'png' : 'jpg' - // await fs.outputFile(`./pictures/profile/${userId}`, file.buffer); let quality = 50; const output = await sharp(file.buffer) .resize({ width: 300 }) @@ -45,7 +43,6 @@ export class FileService { } async storeCompanyLogo(file: Express.Multer.File, userId: string) { - // const suffix = file.mimetype.includes('png') ? 'png' : 'jpg' let quality = 50; const output = await sharp(file.buffer) .resize({ width: 300 }) @@ -110,17 +107,11 @@ export class FileService { let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen let output; let start = Date.now(); - // do { output = await sharp(buffer) .resize({ width: 1500 }) .avif({ quality }) // Verwende AVIF //.webp({ quality }) // Verwende Webp .toBuffer(); - - // if (output.byteLength > maxSize) { - // quality -= 5; // Justiere Qualität in feineren Schritten - // } - // } while (output.byteLength > maxSize && quality > 0); await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung let timeTaken = Date.now() - start; this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`) @@ -141,4 +132,8 @@ export class FileService { } return result; } + deleteImage(path:string){ + fs.unlinkSync(path); + } + } diff --git a/bizmatch-server/src/image/image.controller.ts b/bizmatch-server/src/image/image.controller.ts index 55bb6cb..01d14ac 100644 --- a/bizmatch-server/src/image/image.controller.ts +++ b/bizmatch-server/src/image/image.controller.ts @@ -3,24 +3,22 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { FileInterceptor } from '@nestjs/platform-express'; import { FileService } from '../file/file.service.js'; +import { SelectOptionsService } from '../select-options/select-options.service.js'; @Controller('image') export class ImageController { - constructor(private fileService:FileService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { + constructor(private fileService:FileService, + @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, + private selectOptions:SelectOptionsService) { } @Post('uploadPropertyPicture/:id') @UseInterceptors(FileInterceptor('file'),) - async uploadFile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { + async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { await this.fileService.storePropertyPicture(file,id); } - @Get(':id') - async getPropertyImagesById(@Param('id') id:string): Promise { - return await this.fileService.getPropertyImages(id); - } - @Post('uploadProfile/:id') @UseInterceptors(FileInterceptor('file'),) async uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { @@ -32,7 +30,11 @@ export class ImageController { async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { await this.fileService.storeCompanyLogo(file,id); } - + + @Get(':id') + async getPropertyImagesById(@Param('id') id:string): Promise { + return await this.fileService.getPropertyImages(id); + } @Get('profileImages/:userids') async getProfileImagesForUsers(@Param('userids') userids:string): Promise { return await this.fileService.getProfileImagesForUsers(userids); @@ -41,4 +43,16 @@ export class ImageController { async getCompanyLogosForUsers(@Param('userids') userids:string): Promise { return await this.fileService.getCompanyLogosForUsers(userids); } + @Delete('propertyPicture/:listingid/:imagename') + async deletePropertyImagesById(@Param('listingid') listingid:string,@Param('imagename') imagename:string): Promise { + this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`) + } + @Delete('logo/:userid/') + async deleteLogoImagesById(@Param('id') id:string): Promise { + this.fileService.deleteImage(`pictures/property//${id}`) + } + @Delete('profile/:userid/') + async deleteProfileImagesById(@Param('id') id:string): Promise { + this.fileService.deleteImage(`pictures/property//${id}`) + } } diff --git a/bizmatch-server/src/image/image.module.ts b/bizmatch-server/src/image/image.module.ts index a202fb3..c9db704 100644 --- a/bizmatch-server/src/image/image.module.ts +++ b/bizmatch-server/src/image/image.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { ImageController } from './image.controller.js'; import { ImageService } from './image.service.js'; import { FileService } from '../file/file.service.js'; +import { SelectOptionsService } from '../select-options/select-options.service.js'; @Module({ controllers: [ImageController], - providers: [ImageService,FileService] + providers: [ImageService,FileService,SelectOptionsService] }) export class ImageModule {} diff --git a/bizmatch-server/src/select-options/select-options.service.ts b/bizmatch-server/src/select-options/select-options.service.ts index e8bfaf2..77603dc 100644 --- a/bizmatch-server/src/select-options/select-options.service.ts +++ b/bizmatch-server/src/select-options/select-options.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { KeyValue, KeyValueStyle } from '../models/main.model.js'; +import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js'; @Injectable() export class SelectOptionsService { @@ -45,6 +45,11 @@ export class SelectOptionsService { { name: 'Broker', value: 'broker', icon:'pi-image',bgColorClass:'bg-green-100',textColorClass:'text-green-600' }, { name: 'Professional', value: 'professional', icon:'pi-globe',bgColorClass:'bg-yellow-100',textColorClass:'text-yellow-600' }, ] + public imageTypes:ImageType[] = [ + {name:'propertyPicture',upload:'uploadPropertyPicture',delete:'propertyPicture'}, + {name:'companyLogo',upload:'uploadCompanyLogo',delete:'logo'}, + {name:'profile',upload:'uploadProfile',delete:'profile'}, + ] private usStates = [ { name: 'ALABAMA', abbreviation: 'AL'}, { name: 'ALASKA', abbreviation: 'AK'}, diff --git a/bizmatch/package.json b/bizmatch/package.json index 0977085..008142f 100644 --- a/bizmatch/package.json +++ b/bizmatch/package.json @@ -13,6 +13,7 @@ "private": true, "dependencies": { "@angular/animations": "^17.2.2", + "@angular/cdk": "^17.3.2", "@angular/common": "^17.2.2", "@angular/compiler": "^17.2.2", "@angular/core": "^17.2.2", diff --git a/bizmatch/src/app/components/image-cropper/image-cropper.component.html b/bizmatch/src/app/components/image-cropper/image-cropper.component.html new file mode 100644 index 0000000..1585625 --- /dev/null +++ b/bizmatch/src/app/components/image-cropper/image-cropper.component.html @@ -0,0 +1,15 @@ + +
+ @if(ratioVariable){ +
+ +
+ } @else { +
+ } +
+ + +
+
\ No newline at end of file diff --git a/bizmatch/src/app/components/image-cropper/image-cropper.component.scss b/bizmatch/src/app/components/image-cropper/image-cropper.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/bizmatch/src/app/components/image-cropper/image-cropper.component.ts b/bizmatch/src/app/components/image-cropper/image-cropper.component.ts new file mode 100644 index 0000000..e44c561 --- /dev/null +++ b/bizmatch/src/app/components/image-cropper/image-cropper.component.ts @@ -0,0 +1,64 @@ +import { Component, ViewChild } from '@angular/core'; +import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs'; +import { LoadingService } from '../../services/loading.service'; +import { ImageService } from '../../services/image.service'; +import { HttpEventType } from '@angular/common/http'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { FileUpload, FileUploadModule } from 'primeng/fileupload'; +import { environment } from '../../../environments/environment'; +import { KeyValueRatio, User } from '../../../../../common-models/src/main.model'; +import { SharedModule } from '../../shared/shared/shared.module'; +import { SelectButtonModule } from 'primeng/selectbutton'; +export const stateOptions:KeyValueRatio[]=[ + {label:'16/9',value:16/9}, + {label:'1/1',value:1}, + {label:'2/3',value:2/3}, +] +@Component({ + selector: 'app-image-cropper', + standalone: true, + imports: [SharedModule,FileUploadModule,AngularCropperjsModule,SelectButtonModule], + templateUrl: './image-cropper.component.html', + styleUrl: './image-cropper.component.scss' +}) +export class ImageCropperComponent { + @ViewChild(CropperComponent) public angularCropper: CropperComponent; + imageUrl:string; //wird im Template verwendet + fileUpload:FileUpload + value:number = stateOptions[0].value; + cropperConfig={aspectRatio: this.value} + ratioVariable:boolean + stateOptions=stateOptions + constructor( + private loadingService:LoadingService, + private imageUploadService: ImageService, + public config: DynamicDialogConfig, + public ref: DynamicDialogRef + ){} + ngOnInit(): void { + if (this.config.data) { + this.imageUrl = this.config.data.imageUrl; + this.fileUpload = this.config.data.fileUpload; + this.cropperConfig = this.config.data.config ? this.config.data.config: this.cropperConfig; + this.ratioVariable = this.config.data.ratioVariable; + } + } + sendImage(){ + this.loadingService.startLoading('uploadImage'); + setTimeout(()=>{ + this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => { + this.fileUpload.clear() + this.ref.close(blob); + }, 'image/png'); + }) + } + + cancelUpload(){ + this.fileUpload.clear(); + this.ref.close(); + } + changeAspectRation(ratio:number){ + this.cropperConfig={aspectRatio: ratio} + this.angularCropper.cropper.setAspectRatio(ratio); + } +} diff --git a/bizmatch/src/app/pages/details/details-listing/details-listing.component.html b/bizmatch/src/app/pages/details/details-listing/details-listing.component.html index 254a597..b9969e9 100644 --- a/bizmatch/src/app/pages/details/details-listing/details-listing.component.html +++ b/bizmatch/src/app/pages/details/details-listing/details-listing.component.html @@ -13,7 +13,7 @@
  • Description
    -
    {{listing?.description}}
    +
  • @if (listing && (listing.listingsCategory==='business')){
  • diff --git a/bizmatch/src/app/pages/details/details-listing/details-listing.component.ts b/bizmatch/src/app/pages/details/details-listing/details-listing.component.ts index 7f55a5d..d0e20fc 100644 --- a/bizmatch/src/app/pages/details/details-listing/details-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-listing/details-listing.component.ts @@ -24,6 +24,7 @@ import { MessageService } from 'primeng/api'; import { SharedModule } from '../../../shared/shared/shared.module'; import { GalleriaModule } from 'primeng/galleria'; import { environment } from '../../../../environments/environment'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @Component({ selector: 'app-details-listing', standalone: true, @@ -59,13 +60,15 @@ export class DetailsListingComponent { propertyImages: ImageProperty[] = [] environment = environment; user:User + description:SafeHtml; constructor(private activatedRoute: ActivatedRoute, private listingsService: ListingsService, private router: Router, private userService: UserService, public selectOptions: SelectOptionsService, private mailService: MailService, - private messageService: MessageService) { + private messageService: MessageService, + private sanitizer: DomSanitizer) { this.userService.getUserObservable().subscribe(user => { this.user = user }); @@ -76,6 +79,7 @@ export class DetailsListingComponent { async ngOnInit() { this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, this.type)); this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) + this.description=this.sanitizer.bypassSecurityTrustHtml(this.listing.description); } back() { this.router.navigate(['listings', this.criteria.listingsCategory]) diff --git a/bizmatch/src/app/pages/subscription/account/account.component.html b/bizmatch/src/app/pages/subscription/account/account.component.html index cc856a6..c837f9a 100644 --- a/bizmatch/src/app/pages/subscription/account/account.component.html +++ b/bizmatch/src/app/pages/subscription/account/account.component.html @@ -56,34 +56,14 @@
    - - - - - - - - - - + +
    - - - - - - - - - - + +
    @@ -126,7 +106,7 @@ Company Logo (is shown in every offer) @if(user.hasCompanyLogo){ - + } @else { } @@ -138,7 +118,7 @@
    Your Profile Picture @if(user.hasProfile){ - + } @else { } @@ -212,8 +192,7 @@
    - - + \ No newline at end of file diff --git a/bizmatch/src/app/pages/subscription/account/account.component.ts b/bizmatch/src/app/pages/subscription/account/account.component.ts index cb87c63..bd2a87e 100644 --- a/bizmatch/src/app/pages/subscription/account/account.component.ts +++ b/bizmatch/src/app/pages/subscription/account/account.component.ts @@ -32,69 +32,64 @@ import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs'; import { ImageService } from '../../../services/image.service'; import { DialogModule } from 'primeng/dialog'; import { SelectButtonModule } from 'primeng/selectbutton'; +import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component'; +import Quill from 'quill' +import { TOOLBAR_OPTIONS } from '../../utils/defaults'; @Component({ selector: 'app-account', standalone: true, - imports: [SharedModule,FileUploadModule,EditorModule,AngularCropperjsModule,DialogModule,SelectButtonModule], - providers:[MessageService], + imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule], + providers: [MessageService, DialogService], templateUrl: './account.component.html', styleUrl: './account.component.scss' }) export class AccountComponent { - @ViewChild(CropperComponent) public angularCropper: CropperComponent; @ViewChild('companyUpload') public companyUpload: FileUpload; @ViewChild('profileUpload') public profileUpload: FileUpload; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; - user:User; - subscriptions:Array; - userSubscriptions:Array=[]; - maxFileSize=1000000; - companyLogoUrl:string; - profileUrl:string; - imageUrl; - type:'company'|'profile' - stateOptions:KeyValueRatio[]=[ - {label:'16/9',value:16/9}, - {label:'1/1',value:1}, - {label:'2/3',value:2/3}, - ] - value:number = this.stateOptions[0].value; - config={aspectRatio: this.value} - environment=environment - constructor(public userService: UserService, - private subscriptionService: SubscriptionsService, - private messageService: MessageService, - private geoService:GeoService, - public selectOptions:SelectOptionsService, - private cdref:ChangeDetectorRef, - private activatedRoute: ActivatedRoute, - private loadingService:LoadingService, - private imageUploadService: ImageService) { - - } - async ngOnInit(){ - this.user=await this.userService.getById(this.id); - this.userSubscriptions=await lastValueFrom(this.subscriptionService.getAllSubscriptions()); - if (!this.user.licensedIn || this.user.licensedIn?.length===0){ - this.user.licensedIn = [{name:'',value:''}] + user: User; + subscriptions: Array; + userSubscriptions: Array = []; + maxFileSize = 1000000; + companyLogoUrl: string; + profileUrl: string; + type: 'company' | 'profile' + dialogRef: DynamicDialogRef | undefined; + environment = environment + editorModules = TOOLBAR_OPTIONS + constructor(public userService: UserService, + private subscriptionService: SubscriptionsService, + private messageService: MessageService, + private geoService: GeoService, + public selectOptions: SelectOptionsService, + private cdref: ChangeDetectorRef, + private activatedRoute: ActivatedRoute, + private loadingService: LoadingService, + private imageUploadService: ImageService, + public dialogService: DialogService) {} + async ngOnInit() { + this.user = await this.userService.getById(this.id); + this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions()); + if (!this.user.licensedIn || this.user.licensedIn?.length === 0) { + this.user.licensedIn = [{ name: '', value: '' }] } - this.user=await this.userService.getById(this.user.id); - this.profileUrl = this.user.hasProfile?`${environment.apiBaseUrl}/profile/${this.user.id}.avif`:`/assets/images/placeholder.png` - this.companyLogoUrl = this.user.hasCompanyLogo?`${environment.apiBaseUrl}/logo/${this.user.id}.avif`:`/assets/images/placeholder.png` + this.user = await this.userService.getById(this.user.id); + this.profileUrl = this.user.hasProfile ? `${environment.apiBaseUrl}/profile/${this.user.id}.avif` : `/assets/images/placeholder.png` + this.companyLogoUrl = this.user.hasCompanyLogo ? `${environment.apiBaseUrl}/logo/${this.user.id}.avif` : `/assets/images/placeholder.png` } - printInvoice(invoice:Invoice){} + printInvoice(invoice: Invoice) { } - async updateProfile(user:User){ - //this.messageService.add({ severity: 'warn', summary: 'Information', detail: 'This function is not yet available, please send an email to info@bizmatch.net for changes to your customer data', life: 15000 }); + async updateProfile(user: User) { await this.userService.save(this.user); } - onUploadCompanyLogo(event:any){ + onUploadCompanyLogo(event: any) { const uniqueSuffix = '?_ts=' + new Date().getTime(); this.companyLogoUrl = `${environment.apiBaseUrl}/logo/${this.user.id}${uniqueSuffix}` //`http://IhrServer:Port/${newImagePath}${uniqueSuffix}`; } - onUploadProfilePicture(event:any){ + onUploadProfilePicture(event: any) { const uniqueSuffix = '?_ts=' + new Date().getTime(); this.profileUrl = `${environment.apiBaseUrl}/profile/${this.user.id}${uniqueSuffix}` //`http://IhrServer:Port/${newImagePath}${uniqueSuffix}`; } @@ -102,73 +97,63 @@ export class AccountComponent { (event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild } - + suggestions: string[] | undefined; async search(event: AutoCompleteCompleteEvent) { - const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query)) - this.suggestions = result.map(r=>`${r.city} - ${r.state_code}`).slice(0,5); + const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query)) + this.suggestions = result.map(r => `${r.city} - ${r.state_code}`).slice(0, 5); } - addLicence(){ - this.user.licensedIn.push({name:'',value:''}); + addLicence() { + this.user.licensedIn.push({ name: '', value: '' }); } - removeLicence(){ - this.user.licensedIn.splice(this.user.licensedIn.length-2,1); + removeLicence() { + this.user.licensedIn.splice(this.user.licensedIn.length - 2, 1); } - select(event:any,type:'company'|'profile'){ - this.imageUrl = URL.createObjectURL(event.files[0]); - this.type=type - this.config={aspectRatio: type==='company'?this.stateOptions[0].value:this.stateOptions[2].value} - } - sendImage(){ - this.imageUrl=null - this.loadingService.startLoading('uploadImage'); - this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => { - if (this.type==='company'){ - this.imageUploadService.uploadCompanyLogo(blob,this.user.id).subscribe(async(event) => { + select(event: any, type: 'company' | 'profile') { + const imageUrl = URL.createObjectURL(event.files[0]); + this.type = type + const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value } + this.dialogRef = this.dialogService.open(ImageCropperComponent, { + data: { + imageUrl: imageUrl, + fileUpload: type === 'company' ? this.companyUpload : this.profileUpload, + config: config, + ratioVariable: type === 'company' ? true : false + }, + header: 'Edit Image', + width: '50vw', + modal: true, + closeOnEscape: true, + keepInViewport: true, + closable: false, + breakpoints: { + '960px': '75vw', + '640px': '90vw' + }, + }); + this.dialogRef.onClose.subscribe(blob => { + if (blob) { + this.imageUploadService.uploadImage(blob, type==='company'?'uploadCompanyLogo':'uploadProfile',this.user.id).subscribe(async(event) => { if (event.type === HttpEventType.UploadProgress) { - // Berechne und zeige den Fortschritt basierend auf event.loaded und event.total const progress = event.total ? event.loaded / event.total : 0; console.log(`Upload-Fortschritt: ${progress * 100}%`); - // Hier könntest du beispielsweise eine Fortschrittsanzeige aktualisieren } else if (event.type === HttpEventType.Response) { console.log('Upload abgeschlossen', event.body); - this.companyUpload.clear(); this.loadingService.stopLoading('uploadImage'); - this.companyLogoUrl=`${environment.apiBaseUrl}/logo/${this.user.id}.avif?_ts=${new Date().getTime()}` - } - }, error => console.error('Fehler beim Upload:', error)); - } else { - this.imageUploadService.uploadProfileImage(blob,this.user.id).subscribe(async(event) => { - if (event.type === HttpEventType.UploadProgress) { - // Berechne und zeige den Fortschritt basierend auf event.loaded und event.total - const progress = event.total ? event.loaded / event.total : 0; - console.log(`Upload-Fortschritt: ${progress * 100}%`); - // Hier könntest du beispielsweise eine Fortschrittsanzeige aktualisieren - } else if (event.type === HttpEventType.Response) { - console.log('Upload abgeschlossen', event.body); - this.profileUpload.clear(); - this.loadingService.stopLoading('uploadImage'); - this.profileUrl=`${environment.apiBaseUrl}/profile/${this.user.id}.avif?_ts=${new Date().getTime()}` + if (this.type==='company'){ + this.user.hasCompanyLogo=true; + this.companyLogoUrl=`${environment.apiBaseUrl}/logo/${this.user.id}.avif?_ts=${new Date().getTime()}` + } else { + this.user.hasProfile=true; + this.profileUrl=`${environment.apiBaseUrl}/profile/${this.user.id}.avif?_ts=${new Date().getTime()}` + } } }, error => console.error('Fehler beim Upload:', error)); } - - - // this.fileUpload.upload(); - }, 'image/png'); - } - cancelUpload(){ - this.imageUrl=null - if (this.type==='company'){ - this.companyUpload.clear(); - } else { - this.profileUpload.clear(); - } - } - changeAspectRation(ratio:number){ - this.config={aspectRatio: ratio} - this.angularCropper.cropper.setAspectRatio(ratio); + + }); } + } diff --git a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html index 440e631..5312f7e 100644 --- a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html +++ b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.html @@ -20,7 +20,10 @@
    - + + + +
    @if (listing.listingsCategory==='business'){ @@ -77,7 +80,7 @@
    - Property Picture + Property Pictures
    - + +
    +
      +
    • + + + +
    • +
    +
    } @if (listing.listingsCategory==='business'){
    @@ -195,12 +208,5 @@
    - - - - - - - - - + + \ No newline at end of file diff --git a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.scss b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.scss index a4111ab..9779c9c 100644 --- a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.scss +++ b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.scss @@ -1,10 +1,87 @@ -.translate-y-5{ +.translate-y-5 { transform: translateY(5px); } -.image { - width: 120px; - height: 30px; - border: 1px solid #6b7280; - padding: 1px 1px; - object-fit: contain; - } \ No newline at end of file + +// .image { +// width: 120px; +// height: 30px; +// border: 1px solid #6b7280; +// padding: 1px 1px; +// object-fit: contain; +// } + +// .image-container img { +// width: 200px; +// box-shadow: 0 3px 6px #00000029, 0 3px 6px #0000003b; +// margin-right: 1rem; +// } + +// .container { +// width: 100%; +// min-height: 200px; +// border: 1px solid #ccc; +// display: flex; +// flex-wrap: wrap; +// } +.image-container { + width: 100%; + /* oder eine spezifische Breite */ + overflow-x: auto; + /* Ermöglicht das Scrollen, wenn die Bilder zu breit sind */ +} + +.image-container ul { + display: flex; + padding: 0; + /* Entfernt den Standard-Abstand des ul-Elements */ + margin: 0; + /* Entfernt den Standard-Außenabstand des ul-Elements */ + list-style-type: none; + /* Entfernt die Listenpunkte */ +} + +.image-container li { + flex: 1 1 auto; + /* Erlaubt den li-Elementen, zu wachsen und zu schrumpfen, aber füllt den Raum gleichmäßig */ + /* Optional: Füge hier Abstände zwischen den li-Elementen hinzu */ +} + +.image-container img { + max-width: 100%; + /* Stellt sicher, dass die Bilder nicht über ihre natürliche Größe hinaus wachsen */ + height: auto; + /* Behält das Seitenverhältnis bei */ + cursor: pointer; +} + +.draggable-image { + margin: 8px; + cursor: grab; +} + +.draggable-image:active { + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +// .cdk-drag-preview { +// box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2); +// } + +// .cdk-drag-placeholder { +// background-color: #ccc; +// } + +.drop-area { + border: 2px dashed #ccc; + padding: 20px; + text-align: center; + transition: all 0.3s; +} + +/* CSS-Klasse für den Drop-Bereich, wenn ein Element darüber gezogen wird */ +.drop-area-active { + border-color: #2196F3; + background-color: #E3F2FD; +} \ No newline at end of file diff --git a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts index dafc117..98f0c8e 100644 --- a/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-listing/edit-listing.component.ts @@ -24,7 +24,7 @@ import { lastValueFrom } from 'rxjs'; import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; -import { MessageService } from 'primeng/api'; +import { ConfirmationService, MessageService } from 'primeng/api'; import { AutoCompleteCompleteEvent, BusinessListing, CommercialPropertyListing, ImageProperty, ListingType, User } from '../../../../../../common-models/src/main.model'; import { GeoResult, GeoService } from '../../../services/geo.service'; import { InputNumberComponent, InputNumberModule } from '../../../components/inputnumber/inputnumber.component'; @@ -35,121 +35,177 @@ import { v4 as uuidv4 } from 'uuid'; import { DialogModule } from 'primeng/dialog'; import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs'; import { HttpClient, HttpEventType } from '@angular/common/http'; -import {ImageService} from '../../../services/image.service' +import { ImageService } from '../../../services/image.service' import { LoadingService } from '../../../services/loading.service'; +import { TOOLBAR_OPTIONS } from '../../utils/defaults'; +import { EditorModule } from 'primeng/editor'; +import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { CdkDragDrop, CdkDragEnter, CdkDragExit, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop'; @Component({ selector: 'create-listing', standalone: true, - imports: [SharedModule,ArrayToStringPipe, InputNumberModule,CarouselModule,DialogModule,AngularCropperjsModule,FileUploadModule], - providers:[MessageService], + imports: [SharedModule, ArrayToStringPipe, InputNumberModule, CarouselModule, DialogModule, AngularCropperjsModule, FileUploadModule, EditorModule, DynamicDialogModule, ConfirmDialogModule,DragDropModule], + providers: [MessageService, DialogService, ConfirmationService], templateUrl: './edit-listing.component.html', styleUrl: './edit-listing.component.scss' }) export class EditListingComponent { - @ViewChild(CropperComponent) public angularCropper: CropperComponent; @ViewChild(FileUpload) public fileUpload: FileUpload; - listingCategory:'Business'|'Commercial Property'; - category:string; - location:string; - mode:'edit'|'create'; - separator:'\n\n' - listing:ListingType + listingCategory: 'Business' | 'Commercial Property'; + category: string; + location: string; + mode: 'edit' | 'create'; + separator: '\n\n' + listing: ListingType private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; - user:User; - maxFileSize=3000000; - uploadUrl:string; - environment=environment; - propertyImages:ImageProperty[] + user: User; + maxFileSize = 3000000; + uploadUrl: string; + environment = environment; + propertyImages: ImageProperty[] responsiveOptions = [ { - breakpoint: '1199px', - numVisible: 1, - numScroll: 1 + breakpoint: '1199px', + numVisible: 1, + numScroll: 1 }, { - breakpoint: '991px', - numVisible: 2, - numScroll: 1 + breakpoint: '991px', + numVisible: 2, + numScroll: 1 }, { - breakpoint: '767px', - numVisible: 1, - numScroll: 1 + breakpoint: '767px', + numVisible: 1, + numScroll: 1 } ]; - imageUrl - config={aspectRatio: 16 / 9} - constructor(public selectOptions:SelectOptionsService, - private router: Router, - private activatedRoute: ActivatedRoute, - private listingsService:ListingsService, - public userService: UserService, - private messageService: MessageService, - private geoService:GeoService, - private imageUploadService: ImageService, - private loadingService:LoadingService){ - this.user=this.userService.getUser(); + config = { aspectRatio: 16 / 9 } + editorModules = TOOLBAR_OPTIONS + dialogRef: DynamicDialogRef | undefined; + draggedImage:ImageProperty + dropAreaActive = false; + constructor(public selectOptions: SelectOptionsService, + private router: Router, + private activatedRoute: ActivatedRoute, + private listingsService: ListingsService, + public userService: UserService, + private messageService: MessageService, + private geoService: GeoService, + private imageService: ImageService, + private loadingService: LoadingService, + public dialogService: DialogService, + private confirmationService: ConfirmationService) { + this.user = this.userService.getUser(); // Abonniere Router-Events, um den aktiven Link zu ermitteln this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { - this.mode = event.url==='/createListing'?'create':'edit'; + this.mode = event.url === '/createListing' ? 'create' : 'edit'; } }); } - async ngOnInit(){ - if (this.mode==='edit'){ - this.listing=await lastValueFrom(this.listingsService.getListingById(this.id)); + async ngOnInit() { + if (this.mode === 'edit') { + this.listing = await lastValueFrom(this.listingsService.getListingById(this.id)); } else { - const uuid = sessionStorage.getItem('uuid')?sessionStorage.getItem('uuid'):uuidv4(); - sessionStorage.setItem('uuid',uuid); - this.listing=createGenericObject(); - this.listing.id=uuid - this.listing.temporary=true; - this.listing.userId=this.user.id - this.listing.listingsCategory='business'; - } + const uuid = sessionStorage.getItem('uuid') ? sessionStorage.getItem('uuid') : uuidv4(); + sessionStorage.setItem('uuid', uuid); + this.listing = createGenericObject(); + this.listing.id = uuid + this.listing.temporary = true; + this.listing.userId = this.user.id + this.listing.listingsCategory = 'business'; + } this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`; - this.propertyImages=await this.listingsService.getPropertyImages(this.listing.id) + this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) } - async save(){ + async save() { sessionStorage.removeItem('uuid') - await this.listingsService.save(this.listing,this.listing.listingsCategory); + await this.listingsService.save(this.listing, this.listing.listingsCategory); this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 }); } suggestions: string[] | undefined; async search(event: AutoCompleteCompleteEvent) { - const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query,this.listing.state)) - this.suggestions = result.map(r=>r.city).slice(0,5); + const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state)) + this.suggestions = result.map(r => r.city).slice(0, 5); } - select(event:any){ - this.imageUrl = URL.createObjectURL(event.files[0]); - } - sendImage(){ - this.imageUrl=null - this.loadingService.startLoading('uploadImage'); - this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => { + select(event: any) { + const imageUrl = URL.createObjectURL(event.files[0]); + this.dialogRef = this.dialogService.open(ImageCropperComponent, { + data: { + imageUrl: imageUrl, + fileUpload: this.fileUpload, + ratioVariable: false + }, + header: 'Edit Image', + width: '50vw', + modal: true, + closeOnEscape: true, + keepInViewport: true, + closable: false, + breakpoints: { + '960px': '75vw', + '640px': '90vw' + }, + }); + this.dialogRef.onClose.subscribe(blob => { + if (blob) { + this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.id).subscribe(async (event) => { + if (event.type === HttpEventType.Response) { + console.log('Upload abgeschlossen', event.body); + this.loadingService.stopLoading('uploadImage'); + this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) + } + }, error => console.error('Fehler beim Upload:', error)); + } - this.imageUploadService.uploadPropertyImage(blob,this.listing.id).subscribe(async(event) => { - if (event.type === HttpEventType.UploadProgress) { - // Berechne und zeige den Fortschritt basierend auf event.loaded und event.total - const progress = event.total ? event.loaded / event.total : 0; - console.log(`Upload-Fortschritt: ${progress * 100}%`); - // Hier könntest du beispielsweise eine Fortschrittsanzeige aktualisieren - } else if (event.type === HttpEventType.Response) { - console.log('Upload abgeschlossen', event.body); - // Hier könntest du die Ladeanimation ausblenden - this.propertyImages=await this.listingsService.getPropertyImages(this.listing.id) - this.fileUpload.clear(); - this.loadingService.stopLoading('uploadImage'); - } - }, error => console.error('Fehler beim Upload:', error)); - - // this.fileUpload.upload(); - }, 'image/png'); + }); } + + deleteConfirm(imageName: string) { + this.confirmationService.confirm({ + target: event.target as EventTarget, + message: `Do you want to delete this image ${imageName}?`, + header: 'Delete Confirmation', + icon: 'pi pi-info-circle', + acceptButtonStyleClass: "p-button-danger p-button-text", + rejectButtonStyleClass: "p-button-text p-button-text", + acceptIcon: "none", + rejectIcon: "none", + + accept: async () => { + await this.imageService.deleteListingImage(this.listing.id, imageName); + this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' }); + this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) + + }, + reject: () => { + // this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' }); + console.log('deny') + } + }); + } + + onDrop(event: CdkDragDrop) { + this.dropAreaActive = false; + moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex); + //console.log(event.previousIndex, event.currentIndex); + } + + + onDragEnter(event: CdkDragEnter) { + this.dropAreaActive = true; + } + + onDragExit(event: CdkDragExit) { + this.dropAreaActive = false; + } + } diff --git a/bizmatch/src/app/pages/utils/defaults.ts b/bizmatch/src/app/pages/utils/defaults.ts new file mode 100644 index 0000000..4c37667 --- /dev/null +++ b/bizmatch/src/app/pages/utils/defaults.ts @@ -0,0 +1,9 @@ +export const TOOLBAR_OPTIONS = { + toolbar: [ + ['bold', 'italic', 'underline'], // Einige Standardoptionen + [{'header': [1, 2, 3, false]}], // Benutzerdefinierte Header + [{'list': 'ordered'}, {'list': 'bullet'}], + [{'color': []}], // Dropdown mit Standardfarben + ['clean'] // Entfernt Formatierungen + ] +}; \ No newline at end of file diff --git a/bizmatch/src/app/services/image.service.ts b/bizmatch/src/app/services/image.service.ts index c584c43..2902ab0 100644 --- a/bizmatch/src/app/services/image.service.ts +++ b/bizmatch/src/app/services/image.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; import { lastValueFrom } from 'rxjs'; +import { ImageType } from '../../../../common-models/src/main.model'; @Injectable({ providedIn: 'root' @@ -12,19 +13,8 @@ export class ImageService { constructor(private http: HttpClient) { } - uploadPropertyImage(imageBlob: Blob,listingId:string) { - const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${listingId}`; - return this.uploadImage(imageBlob,uploadUrl); - } - uploadCompanyLogo(imageBlob: Blob,userId:string) { - const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/uploadCompanyLogo/${userId}`; - return this.uploadImage(imageBlob,uploadUrl); - } - uploadProfileImage(imageBlob: Blob,userId:string) { - const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/uploadProfile/${userId}`; - return this.uploadImage(imageBlob,uploadUrl); - } - uploadImage(imageBlob: Blob,uploadUrl:string) { + uploadImage(imageBlob: Blob,type:'uploadPropertyPicture'|'uploadCompanyLogo'|'uploadProfile',id:string) { + const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${id}`; const formData = new FormData(); formData.append('file', imageBlob, 'image.png'); @@ -34,7 +24,12 @@ export class ImageService { observe: 'events', }); } - + async deleteUserImage(userid:string,type:ImageType,name?:string){ + return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/${type.delete}${userid}`)); + } + async deleteListingImage(listingid:string,name?:string){ + return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${listingid}/${name}`)); + } async getProfileImagesForUsers(userids:string[]){ return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/profileImages/${userids.join(',')}`)); } diff --git a/bizmatch/src/app/services/loading.service.ts b/bizmatch/src/app/services/loading.service.ts index a5b7cf6..f4179f3 100644 --- a/bizmatch/src/app/services/loading.service.ts +++ b/bizmatch/src/app/services/loading.service.ts @@ -20,7 +20,10 @@ export class LoadingService { public startLoading(type: string,request?:string): void { if (!this.loading$.value.includes(type)) { this.loading$.next(this.loading$.value.concat(type)); - if (type==='uploadImage' || request?.includes('uploadPropertyPicture')|| request?.includes('uploadProfile')|| request?.includes('uploadCompanyLogo')) { + if (type==='uploadImage' + || request?.includes('uploadPropertyPicture') + || request?.includes('uploadProfile') + || request?.includes('uploadCompanyLogo')) { this.loadingTextSubject.next("Please wait - we're processing your image..."); } else { this.loadingTextSubject.next(null); diff --git a/bizmatch/src/styles.scss b/bizmatch/src/styles.scss index aaadb73..ace6ce8 100644 --- a/bizmatch/src/styles.scss +++ b/bizmatch/src/styles.scss @@ -70,4 +70,9 @@ p-menubarsub ul { max-width: 100%; height: 100%; margin: auto; +} +.p-editor-container .ql-toolbar{ + background: #f9fafb; + border-top-right-radius: 6px; + border-top-left-radius: 6px; } \ No newline at end of file diff --git a/common-models/src/main.model.ts b/common-models/src/main.model.ts index 8aef2c5..c2a25a0 100644 --- a/common-models/src/main.model.ts +++ b/common-models/src/main.model.ts @@ -17,12 +17,15 @@ export type SelectOption = { value: T; label: string; }; +export type ImageType = { + name:'propertyPicture'|'companyLogo'|'profile',upload:string,delete:string, +} export interface Listing { id: string; userId: string; type: string; //enum title: string; - description: Array; + description: string; country: string; city: string, state: string;//enum