image-cropper component, drag & drop bilder

This commit is contained in:
Andreas Knuth 2024-03-29 20:59:34 +01:00
parent 840d7a63b1
commit 89bb85a512
20 changed files with 478 additions and 261 deletions

View File

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

View File

@ -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<any> {
return await this.fileService.getPropertyImages(id);
}
@Post('uploadProfile/:id')
@UseInterceptors(FileInterceptor('file'),)
async uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
@ -33,6 +31,10 @@ export class ImageController {
await this.fileService.storeCompanyLogo(file,id);
}
@Get(':id')
async getPropertyImagesById(@Param('id') id:string): Promise<any> {
return await this.fileService.getPropertyImages(id);
}
@Get('profileImages/:userids')
async getProfileImagesForUsers(@Param('userids') userids:string): Promise<any> {
return await this.fileService.getProfileImagesForUsers(userids);
@ -41,4 +43,16 @@ export class ImageController {
async getCompanyLogosForUsers(@Param('userids') userids:string): Promise<any> {
return await this.fileService.getCompanyLogosForUsers(userids);
}
@Delete('propertyPicture/:listingid/:imagename')
async deletePropertyImagesById(@Param('listingid') listingid:string,@Param('imagename') imagename:string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`)
}
@Delete('logo/:userid/')
async deleteLogoImagesById(@Param('id') id:string): Promise<any> {
this.fileService.deleteImage(`pictures/property//${id}`)
}
@Delete('profile/:userid/')
async deleteProfileImagesById(@Param('id') id:string): Promise<any> {
this.fileService.deleteImage(`pictures/property//${id}`)
}
}

View File

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

View File

@ -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'},

View File

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

View File

@ -0,0 +1,15 @@
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="cropperConfig"></angular-cropper>
<div class="flex justify-content-between mt-3">
@if(ratioVariable){
<div>
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)"
optionLabel="label" optionValue="value"></p-selectButton>
</div>
} @else {
<div></div>
}
<div>
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button>
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
</div>
</div>

View File

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

View File

@ -13,7 +13,7 @@
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Description</div>
<div class="text-900 w-full md:w-10 line-height-3">{{listing?.description}}</div>
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
</li>
@if (listing && (listing.listingsCategory==='business')){
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">

View File

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

View File

@ -56,34 +56,14 @@
</div>
<div class="mb-4">
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }">
<ng-template pTemplate="header">
<span class="ql-formats">
<button type="button" class="ql-bold" aria-label="Bold"></button>
<button type="button" class="ql-italic" aria-label="Italic"></button>
<button type="button" class="ql-underline" aria-label="Underline"></button>
<button value="ordered" aria-label="Ordered List" type="button"
class="ql-list"></button>
<button value="bullet" aria-label="Unordered List" type="button"
class="ql-list"></button>
</span>
</ng-template>
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
<div class="mb-4">
<label for="companyOverview" class="block font-medium text-900 mb-2">Services We offer</label>
<p-editor [(ngModel)]="user.offeredServices" [style]="{ height: '320px' }">
<ng-template pTemplate="header">
<span class="ql-formats">
<button type="button" class="ql-bold" aria-label="Bold"></button>
<button type="button" class="ql-italic" aria-label="Italic"></button>
<button type="button" class="ql-underline" aria-label="Underline"></button>
<button value="ordered" aria-label="Ordered List" type="button"
class="ql-list"></button>
<button value="bullet" aria-label="Unordered List" type="button"
class="ql-list"></button>
</span>
</ng-template>
<p-editor [(ngModel)]="user.offeredServices" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
@ -126,7 +106,7 @@
<span class="font-medium text-900 mb-2">Company Logo</span>
<span class="font-medium text-xs mb-2">(is shown in every offer)</span>
@if(user.hasCompanyLogo){
<img src="{{environment.apiBaseUrl}}/logo/{{user.id}}.avif" class="rounded-profile" />
<img src="{{companyLogoUrl}}" class="rounded-profile" />
} @else {
<img src="assets/images/placeholder.png" class="rounded-profile" />
}
@ -138,7 +118,7 @@
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Your Profile Picture</span>
@if(user.hasProfile){
<img src="{{environment.apiBaseUrl}}/profile/{{user.id}}.avif" class="rounded-profile" />
<img src="{{profileUrl}}" class="rounded-profile" />
} @else {
<img src="assets/images/person_placeholder.jpg" class="rounded-profile" />
}
@ -212,8 +192,7 @@
</div>
</div>
</div>
<p-dialog header="Edit Image" [visible]="imageUrl" [modal]="true" [style]="{ width: '50vw' }" [draggable]="false" [resizable]="false">
<!-- <app-cropper #cropper [imageUrl]="imageUrl"></app-cropper> -->
<!-- <p-dialog header="Edit Image" [visible]="imageUrl" [modal]="true" [style]="{ width: '50vw' }" [draggable]="false" [resizable]="false">
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="config"></angular-cropper>
<ng-template pTemplate="footer" let-config="config">
<div class="flex justify-content-between">
@ -230,4 +209,4 @@
</div>
</div>
</ng-template>
</p-dialog>
</p-dialog> -->

View File

@ -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<Subscription>;
userSubscriptions:Array<Subscription>=[];
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
user: User;
subscriptions: Array<Subscription>;
userSubscriptions: Array<Subscription> = [];
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 geoService: GeoService,
public selectOptions: SelectOptionsService,
private cdref: ChangeDetectorRef,
private activatedRoute: ActivatedRoute,
private loadingService:LoadingService,
private imageUploadService: ImageService) {
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`
}
printInvoice(invoice: Invoice) { }
}
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`
}
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}`;
}
@ -107,68 +102,58 @@ export class AccountComponent {
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);
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) => {
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) {
const progress = event.total ? event.loaded / event.total : 0;
console.log(`Upload-Fortschritt: ${progress * 100}%`);
} else if (event.type === HttpEventType.Response) {
console.log('Upload abgeschlossen', event.body);
this.loadingService.stopLoading('uploadImage');
if (this.type==='company'){
this.imageUploadService.uploadCompanyLogo(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.companyUpload.clear();
this.loadingService.stopLoading('uploadImage');
this.user.hasCompanyLogo=true;
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.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);
}
}

View File

@ -20,7 +20,10 @@
<div>
<div class="mb-4">
<label for="description" class="block font-medium text-900 mb-2">Description</label>
<textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea>
<!-- <textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea> -->
<p-editor [(ngModel)]="listing.description" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
</div>
@if (listing.listingsCategory==='business'){
@ -77,7 +80,7 @@
</div>
<div class="mb-4 col-12 md:col-6">
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Property Picture</span>
<span class="font-medium text-900 mb-2">Property Pictures</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload mode="basic"
chooseLabel="Upload"
@ -91,7 +94,7 @@
</div>
</div>
</div>
<p-carousel [value]="propertyImages" [numVisible]="3" [numScroll]="3" [circular]="false" [responsiveOptions]="responsiveOptions">
<!-- <p-carousel [value]="propertyImages" [numVisible]="3" [numScroll]="3" [circular]="false" [responsiveOptions]="responsiveOptions">
<ng-template let-image pTemplate="item">
<div class="border-1 surface-border border-round m-2 text-center py-5 px-3">
<div class="mb-3">
@ -100,12 +103,22 @@
<div>
<div class="mt-5 flex align-items-center justify-content-center gap-2">
<p-button icon="pi pi-file-edit" [rounded]="true" />
<p-button icon="fa-solid fa-trash-can" [rounded]="true" severity="danger"></p-button>
<p-button icon="fa-solid fa-trash-can" [rounded]="true" severity="danger" (click)="deleteConfirm(image.name)"></p-button>
</div>
</div>
</div>
</ng-template>
</p-carousel>
</p-carousel> -->
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropList (cdkDropListDropped)="onDrop($event)"
cdkDropListOrientation="horizontal">
<ul >
<li *ngFor="let image of propertyImages" class="p-2 border-round shadow-1" >
<!-- <div class="image-container"> -->
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{image.name}}" [alt]="image.name" class="shadow-2" cdkDrag/>
<!-- </div> -->
</li>
</ul>
</div>
}
@if (listing.listingsCategory==='business'){
<div class="grid">
@ -195,12 +208,5 @@
</div>
</div>
</div>
<p-dialog header="Edit Image" [visible]="imageUrl" [modal]="true" [style]="{ width: '50vw' }" [draggable]="false" [resizable]="false">
<!-- <app-cropper #cropper [imageUrl]="imageUrl"></app-cropper> -->
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="config"></angular-cropper>
<ng-template pTemplate="footer">
<p-button icon="pi" (click)="imageUrl = null" label="Cancel" [outlined]="true"></p-button>
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
</ng-template>
</p-dialog>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>

View File

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

View File

@ -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,31 +35,36 @@ 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',
@ -77,79 +82,130 @@ export class EditListingComponent {
numScroll: 1
}
];
imageUrl
config={aspectRatio: 16 / 9}
constructor(public selectOptions:SelectOptionsService,
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,
private listingsService: ListingsService,
public userService: UserService,
private messageService: MessageService,
private geoService:GeoService,
private imageUploadService: ImageService,
private loadingService:LoadingService){
this.user=this.userService.getUser();
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<BusinessListing>();
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<BusinessListing>();
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) => {
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) {
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);
// Hier könntest du die Ladeanimation ausblenden
this.propertyImages=await this.listingsService.getPropertyImages(this.listing.id)
this.fileUpload.clear();
this.loadingService.stopLoading('uploadImage');
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
}
}, 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<string[]>) {
this.dropAreaActive = false;
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
//console.log(event.previousIndex, event.currentIndex);
}
onDragEnter(event: CdkDragEnter<any,any>) {
this.dropAreaActive = true;
}
onDragExit(event: CdkDragExit<any,any>) {
this.dropAreaActive = false;
}
}

View File

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

View File

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

View File

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

View File

@ -71,3 +71,8 @@ p-menubarsub ul {
height: 100%;
margin: auto;
}
.p-editor-container .ql-toolbar{
background: #f9fafb;
border-top-right-radius: 6px;
border-top-left-radius: 6px;
}

View File

@ -17,12 +17,15 @@ export type SelectOption<T = number> = {
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<string>;
description: string;
country: string;
city: string,
state: string;//enum