feat: implement image carousel in product dialog; enhance image upload handling and preview functionality
This commit is contained in:
@@ -23,14 +23,98 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thumbs {
|
||||
display: flex;
|
||||
/* ===== Nouveau : carrousel ===== */
|
||||
|
||||
.carousel {
|
||||
grid-column: span 12;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.thumbs img {
|
||||
.carousel-main {
|
||||
position: relative;
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.carousel-main img {
|
||||
max-width: 100%;
|
||||
max-height: 280px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.carousel-nav-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.carousel-nav-btn.left {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.carousel-nav-btn.right {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.carousel-placeholder {
|
||||
text-align: center;
|
||||
color: #757575;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.carousel-placeholder mat-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
/* Bandeau de vignettes */
|
||||
|
||||
.carousel-thumbs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.thumb-item {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .2);
|
||||
overflow: hidden;
|
||||
border: 2px solid transparent;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.thumb-item.active {
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumb-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eeeeee;
|
||||
color: #575656;
|
||||
border: 1px dashed #bdbdbd;
|
||||
}
|
||||
|
||||
.thumb-placeholder mat-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
@@ -2,22 +2,57 @@
|
||||
|
||||
<div mat-dialog-content class="grid" [formGroup]="form">
|
||||
|
||||
<!-- Input pour l'upload des images -->
|
||||
<div class="col-12">
|
||||
<label for="fileInput">Images du produit</label>
|
||||
<input type="file" multiple (change)="onFiles($event)">
|
||||
</div>
|
||||
<!-- CARROUSEL IMAGES -->
|
||||
<div class="col-12 carousel">
|
||||
<div class="carousel-main">
|
||||
|
||||
<!-- Affichage des vignettes des images existantes en mode édition -->
|
||||
@if (mode==='edit' && existingImageUrls.length) {
|
||||
<div class="col-12">
|
||||
<div class="thumbs">
|
||||
@for (url of existingImageUrls; track url) {
|
||||
<img [src]="url" alt="Produit">
|
||||
}
|
||||
</div>
|
||||
<!-- Bouton précédent -->
|
||||
<button mat-icon-button
|
||||
class="carousel-nav-btn left"
|
||||
(click)="prev()"
|
||||
[disabled]="carouselItems.length <= 1">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Image principale ou placeholder -->
|
||||
@if (carouselItems.length && !carouselItems[currentIndex].isPlaceholder) {
|
||||
<img [src]="carouselItems[currentIndex].src" alt="Produit">
|
||||
} @else {
|
||||
<div class="carousel-placeholder" (click)="fileInput.click()">
|
||||
<mat-icon>add_photo_alternate</mat-icon>
|
||||
<span>Ajouter des images</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Bouton suivant -->
|
||||
<button mat-icon-button
|
||||
class="carousel-nav-btn right"
|
||||
(click)="next()"
|
||||
[disabled]="carouselItems.length <= 1">
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Bandeau de vignettes -->
|
||||
<div class="carousel-thumbs">
|
||||
@for (item of carouselItems; let i = $index; track item) {
|
||||
<div class="thumb-item"
|
||||
[class.active]="i === currentIndex"
|
||||
(click)="onThumbClick(i)">
|
||||
@if (!item.isPlaceholder) {
|
||||
<img class="thumb-img" [src]="item.src" alt="Vignette produit">
|
||||
} @else {
|
||||
<div class="thumb-placeholder" (click)="fileInput.click()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Input réel, caché -->
|
||||
<input #fileInput type="file" multiple hidden (change)="onFiles($event)">
|
||||
</div>
|
||||
|
||||
<!-- Input pour le nom du produit -->
|
||||
<mat-form-field class="col-12">
|
||||
@@ -93,6 +128,7 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="close()">Annuler</button>
|
||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.invalid">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component, Inject, OnInit, inject } from '@angular/core';
|
||||
import {Component, Inject, OnInit, inject, OnDestroy} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatButton, MatIconButton } from '@angular/material/button';
|
||||
import {
|
||||
MatDialogRef,
|
||||
MAT_DIALOG_DATA,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
MatDialogContent,
|
||||
MatDialogTitle
|
||||
} from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
|
||||
import { catchError, forkJoin, of, Observable } from 'rxjs';
|
||||
|
||||
@@ -26,6 +27,8 @@ export type ProductDialogData = {
|
||||
productRow?: ProductListItem & { priceHt?: number };
|
||||
};
|
||||
|
||||
type CarouselItem = { src: string; isPlaceholder: boolean };
|
||||
|
||||
@Component({
|
||||
selector: 'app-ps-product-dialog',
|
||||
standalone: true,
|
||||
@@ -34,10 +37,11 @@ export type ProductDialogData = {
|
||||
imports: [
|
||||
CommonModule, ReactiveFormsModule,
|
||||
MatFormField, MatLabel, MatInput, MatSelectModule, MatCheckbox,
|
||||
MatButton, MatDialogActions, MatDialogContent, MatDialogTitle
|
||||
MatButton, MatDialogActions, MatDialogContent, MatDialogTitle,
|
||||
MatIcon, MatIconButton
|
||||
]
|
||||
})
|
||||
export class PsProductDialogComponent implements OnInit {
|
||||
export class PsProductDialogComponent implements OnInit, OnDestroy {
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly ps = inject(PrestashopService);
|
||||
|
||||
@@ -55,6 +59,13 @@ export class PsProductDialogComponent implements OnInit {
|
||||
images: File[] = [];
|
||||
existingImageUrls: string[] = [];
|
||||
|
||||
// Previews des fichiers nouvellement sélectionnés
|
||||
previewUrls: string[] = [];
|
||||
|
||||
// items du carrousel (existants + nouveaux + placeholder)
|
||||
carouselItems: CarouselItem[] = [];
|
||||
currentIndex = 0;
|
||||
|
||||
// options possibles pour l'état (Neuf, Très bon état, etc.)
|
||||
conditionOptions: string[] = [];
|
||||
|
||||
@@ -178,15 +189,80 @@ export class PsProductDialogComponent implements OnInit {
|
||||
});
|
||||
|
||||
this.existingImageUrls = imgs;
|
||||
this.buildCarousel();
|
||||
});
|
||||
} else {
|
||||
// mode création : uniquement placeholder au début
|
||||
this.buildCarousel();
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Carrousel / gestion des fichiers --------
|
||||
|
||||
onFiles(ev: Event) {
|
||||
const fl = (ev.target as HTMLInputElement).files;
|
||||
this.images = fl ? Array.from(fl) : [];
|
||||
|
||||
// Nettoyage des anciens objectURL
|
||||
for(let url of this.previewUrls) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
this.previewUrls = [];
|
||||
this.images = [];
|
||||
|
||||
if (fl) {
|
||||
this.images = Array.from(fl);
|
||||
this.previewUrls = this.images.map(f => URL.createObjectURL(f));
|
||||
}
|
||||
|
||||
this.buildCarousel();
|
||||
|
||||
// si on a ajouté des images, se placer sur la première nouvelle
|
||||
if (this.images.length && this.existingImageUrls.length + this.previewUrls.length > 0) {
|
||||
this.currentIndex = this.existingImageUrls.length; // première nouvelle
|
||||
}
|
||||
}
|
||||
|
||||
private buildCarousel() {
|
||||
const items: CarouselItem[] = [
|
||||
...this.existingImageUrls.map(u => ({ src: u, isPlaceholder: false })),
|
||||
...this.previewUrls.map(u => ({ src: u, isPlaceholder: false }))
|
||||
];
|
||||
|
||||
// placeholder en dernier
|
||||
items.push({ src: '', isPlaceholder: true });
|
||||
|
||||
this.carouselItems = items;
|
||||
if (!this.carouselItems.length) {
|
||||
this.currentIndex = 0;
|
||||
} else if (this.currentIndex >= this.carouselItems.length) {
|
||||
this.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
prev() {
|
||||
if (!this.carouselItems.length) return;
|
||||
this.currentIndex = (this.currentIndex - 1 + this.carouselItems.length) % this.carouselItems.length;
|
||||
}
|
||||
|
||||
next() {
|
||||
if (!this.carouselItems.length) return;
|
||||
this.currentIndex = (this.currentIndex + 1) % this.carouselItems.length;
|
||||
}
|
||||
|
||||
onThumbClick(index: number) {
|
||||
if (index < 0 || index >= this.carouselItems.length) return;
|
||||
this.currentIndex = index;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Nettoyage des objectURL
|
||||
for (let url of this.previewUrls) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Save / close inchangés (à part dto.images) --------
|
||||
|
||||
save() {
|
||||
if (this.form.invalid) return;
|
||||
|
||||
@@ -199,7 +275,7 @@ export class PsProductDialogComponent implements OnInit {
|
||||
categoryId: +v.categoryId!,
|
||||
manufacturerId: +v.manufacturerId!,
|
||||
supplierId: +v.supplierId!,
|
||||
images: this.images,
|
||||
images: this.images, // toujours les fichiers sélectionnés
|
||||
complete: !!v.complete,
|
||||
hasManual: !!v.hasManual,
|
||||
conditionLabel: v.conditionLabel || undefined,
|
||||
|
||||
Reference in New Issue
Block a user