editCommercialProps, confirmationService, MessageService, Drag & Drop

This commit is contained in:
2024-07-09 14:32:20 +02:00
parent e0dbebb61c
commit 7f67b81242
16 changed files with 1062 additions and 50 deletions

View File

@@ -105,7 +105,7 @@
<fa-icon [icon]="faTimes" size="2x"></fa-icon>
</button>
<div class="lg:hidden">
@if (listing && listing.imageOrder.length > 0) {
@if (listing && listing.imageOrder && listing.imageOrder.length > 0) {
<div id="gallery" class="relative w-full" data-carousel="slide">
<div class="relative h-56 overflow-hidden rounded-lg md:h-96">
@for (image of listing.imageOrder; track $index) {
@@ -146,7 +146,7 @@
<div class="w-full lg:w-1/2 mt-6 lg:mt-0">
<div class="hidden lg:block">
@if (listing && listing.imageOrder.length > 0) {
@if (listing && listing.imageOrder && listing.imageOrder.length > 0) {
<div id="gallery" class="relative w-full" data-carousel="slide">
<div class="relative h-56 overflow-hidden rounded-lg md:h-96">
@for (image of listing.imageOrder; track $index) {

View File

@@ -1,7 +1,6 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { KeycloakService } from 'keycloak-angular';
import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
@@ -19,7 +18,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({
selector: 'app-account',
standalone: true,
imports: [SharedModule, AngularCropperjsModule],
imports: [SharedModule],
providers: [],
templateUrl: './account.component.html',
styleUrl: './account.component.scss',

View File

@@ -7,8 +7,6 @@ import { createDefaultBusinessListing, map2User, routeListingWithState } from '.
import { DragDropModule } from '@angular/cdk/drag-drop';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { KeycloakService } from 'keycloak-angular';
import { QuillModule } from 'ngx-quill';
@@ -27,7 +25,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({
selector: 'business-listing',
standalone: true,
imports: [SharedModule, ArrayToStringPipe, AngularCropperjsModule, DragDropModule, MixedCdkDragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule],
imports: [SharedModule, ArrayToStringPipe, DragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule],
providers: [],
templateUrl: './edit-business-listing.component.html',
styleUrl: './edit-business-listing.component.scss',

View File

@@ -146,10 +146,11 @@
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg shadow-md p-6">
<h1 class="text-2xl font-semibold mb-6">Edit Listing</h1>
@if (listing){
<form #listingForm="ngForm">
<div class="mb-4">
<label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label>
<select id="listingsCategory" [(ngModel)]="listing.listingsCategory" name="listingsCategory" class="w-full p-2 border border-gray-300 rounded-md">
<select id="listingsCategory" [ngModel]="listingsCategory" name="listingsCategory" class="w-full p-2 border border-gray-300 rounded-md">
<option value="business">Business</option>
<option value="commercialProperty">Commercial Property</option>
</select>
@@ -220,11 +221,10 @@
</label>
</div>
</div>
<div class="container mx-auto p-4">
<div cdkDropList class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4" (cdkDropListDropped)="drop($event)">
<!-- <div class="container mx-auto p-4">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
@for (image of listing.imageOrder; track listing.imageOrder) {
<div cdkDrag class="relative aspect-video cursor-move">
<!-- <img [src]="image" class="w-full h-full object-cover rounded-lg shadow-md" /> -->
<div class="relative aspect-video cursor-move">
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="w-full h-full object-cover rounded-lg shadow-md" />
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
@@ -234,12 +234,56 @@
</div>
}
</div>
</div> -->
<div class="container mx-auto p-4">
<!-- <div class="grid-container"> -->
<!-- @for (image of listing.imageOrder; track image) {
<div cdkDrag class="grid-item">
<div class="image-box">
<img [src]="getImageUrl(image)" [alt]="image" class="w-full h-full object-cover rounded-lg shadow-md" />
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
</div>
</div>
} -->
<app-drag-drop-mixed [listing]="listing" (imageOrderChanged)="imageOrderChanged($event)" (imageToDelete)="deleteConfirm($event)"></app-drag-drop-mixed>
<!-- </div> -->
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h2 class="text-lg font-semibold mb-2">Property Pictures</h2>
<p class="text-sm text-gray-500 mb-4">(Pictures can be uploaded once the listing is posted initially)</p>
<button
(click)="openFileDialog()"
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg class="mr-2 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Upload
</button>
<input type="file" #fileInput style="display: none" (change)="fileChangeEvent($event)" accept="image/*" />
</div>
@if (mode==='create'){
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Post Listing</button>
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 mt-3 rounded-md hover:bg-blue-600">Post Listing</button>
} @else {
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Update Listing</button>
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 mt-3 rounded-md hover:bg-blue-600">Update Listing</button>
}
</form>
}
</div>
</div>
<!-- Modal -->
<div *ngIf="showModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="bg-white p-5 rounded-lg shadow-xl" style="width: 90%; max-width: 600px">
<h3 class="text-lg font-semibold mb-4">Crop Image</h3>
<image-cropper [imageChangedEvent]="imageChangedEvent" [maintainAspectRatio]="true" [aspectRatio]="16 / 9" format="png" (imageCropped)="imageCropped($event)"></image-cropper>
<div class="mt-4 flex justify-end">
<button (click)="closeModal()" class="mr-2 px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Cancel</button>
<button (click)="uploadImage()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Upload</button>
</div>
</div>
</div>
<app-confirmation></app-confirmation>

View File

@@ -55,3 +55,52 @@
cursor: pointer; /* Verwandelt den Cursor in eine Hand, um Interaktivität anzudeuten */
border-radius: 8px; /* Optional: Abrunden der linken unteren Ecke für ästhetische Zwecke */
}
// -------------------------------
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 16px;
}
.grid-item {
display: flex;
justify-content: center;
align-items: center;
aspect-ratio: 16 / 9;
background-color: #f0f0f0;
border-radius: 8px;
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.grid-item: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);
}
.image-box {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
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-placeholder {
opacity: 0.3;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.grid-container.cdk-drop-list-dragging .grid-item:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

View File

@@ -1,21 +1,25 @@
import { Component } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { createDefaultCommercialPropertyListing, map2User, routeListingWithState } from '../../../utils/utils';
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ViewportRuler } from '@angular/cdk/scrolling';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { NgSelectModule } from '@ng-select/ng-select';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { KeycloakService } from 'keycloak-angular';
import { NgxCurrencyDirective } from 'ngx-currency';
import { ImageCroppedEvent, ImageCropperComponent } from 'ngx-image-cropper';
import { QuillModule } from 'ngx-quill';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
import { DragDropMixedComponent } from '../../../components/drag-drop-mixed/drag-drop-mixed.component';
import { MessageService } from '../../../components/message/message.service';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
@@ -26,13 +30,15 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({
selector: 'commercial-property-listing',
standalone: true,
imports: [SharedModule, ArrayToStringPipe, AngularCropperjsModule, DragDropModule, MixedCdkDragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule],
imports: [SharedModule, ArrayToStringPipe, DragDropModule, QuillModule, NgxCurrencyDirective, NgSelectModule, ImageCropperComponent, ConfirmationComponent, DragDropMixedComponent],
providers: [],
templateUrl: './edit-commercial-property-listing.component.html',
styleUrl: './edit-commercial-property-listing.component.scss',
})
export class EditCommercialPropertyListingComponent {
// @ViewChild(FileUpload) public fileUpload: FileUpload;
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
listingsCategory = 'commercialProperty';
category: string;
location: string;
@@ -74,6 +80,10 @@ export class EditCommercialPropertyListingComponent {
quillModules = {
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
};
showModal = false;
imageChangedEvent: any = '';
croppedImage: Blob | null = null;
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
@@ -87,6 +97,10 @@ export class EditCommercialPropertyListingComponent {
private route: ActivatedRoute,
private keycloakService: KeycloakService,
private cdr: ChangeDetectorRef,
private confirmationService: ConfirmationService,
private messageService: MessageService,
private viewportRuler: ViewportRuler,
) {
// Abonniere Router-Events, um den aktiven Link zu ermitteln
this.router.events.subscribe(event => {
@@ -130,7 +144,43 @@ export class EditCommercialPropertyListingComponent {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
this.suggestions = result.map(r => r.city).slice(0, 5);
}
openFileDialog() {
this.fileInput.nativeElement.click();
}
fileChangeEvent(event: any): void {
this.imageChangedEvent = event;
this.showModal = true;
}
imageCropped(event: ImageCroppedEvent) {
this.croppedImage = event.blob;
}
closeModal() {
this.showModal = false;
this.imageChangedEvent = '';
this.croppedImage = null;
}
uploadImage() {
if (this.croppedImage) {
// Convert base64 to blob
// Replace with your actual API endpoint
this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
async response => {
console.log('Upload successful', response);
setTimeout(async () => {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
});
this.closeModal();
},
error => {
console.error('Upload failed', error);
},
);
}
}
// select(event: any) {
// const imageUrl = URL.createObjectURL(event.files[0]);
// getImageDimensions(imageUrl).then(dimensions => {
@@ -174,36 +224,22 @@ export class EditCommercialPropertyListingComponent {
// });
// }
// 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 () => {
// this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
// await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
// this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
// },
// reject: () => {
// // this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
// console.log('deny');
// },
// });
// }
onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
async deleteConfirm(imageName: string) {
const confirmed = await this.confirmationService.showConfirmation('Are you sure you want to delete this image?');
if (confirmed) {
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
this.messageService.showMessage('Image deleted');
} else {
console.log('deny');
}
}
changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing);
}
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
imageOrderChanged(imageOrder: string[]) {
this.listing.imageOrder = imageOrder;
}
}