364 lines
16 KiB
TypeScript
364 lines
16 KiB
TypeScript
// ============================================
|
|
// components/admin/ProductDetailsModal.tsx
|
|
// ============================================
|
|
// ✅ Modal d'affichage détaillé d'un produit
|
|
// ✅ Galerie de médias (images/vidéos)
|
|
// ✅ Informations complètes
|
|
|
|
import React, { useState } from "react";
|
|
import {
|
|
X,
|
|
Package,
|
|
DollarSign,
|
|
Box,
|
|
Calendar,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Image as ImageIcon,
|
|
Video as VideoIcon,
|
|
Play,
|
|
} from "lucide-react";
|
|
import type { Product, ProductPrice } from "../api/api_types";
|
|
import { getMediaUrl } from "../api/api";
|
|
import "./ProductModal.css";
|
|
|
|
interface ProductDetailsModalProps {
|
|
product: Product;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
|
|
product,
|
|
onClose,
|
|
}) => {
|
|
// ============================================
|
|
// 📝 STATE
|
|
// ============================================
|
|
const [currentMediaIndex, setCurrentMediaIndex] = useState(0);
|
|
|
|
// ============================================
|
|
// 🎨 HELPER FUNCTIONS
|
|
// ============================================
|
|
const getCategoryLabel = (category: string): string => {
|
|
switch (category) {
|
|
case "weed&hash":
|
|
return "Weed & Hash";
|
|
case "zipette&co":
|
|
return "Zipette & Co";
|
|
case "gros&semi":
|
|
return "Gros & Semi";
|
|
default:
|
|
return category;
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString?: string): string => {
|
|
if (!dateString) return "N/A";
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString("fr-FR", {
|
|
day: "2-digit",
|
|
month: "long",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
};
|
|
|
|
const hasMedia = product.media && product.media.length > 0;
|
|
const currentMedia =
|
|
hasMedia && product.media ? product.media[currentMediaIndex] : null;
|
|
|
|
const nextMedia = () => {
|
|
if (
|
|
hasMedia &&
|
|
product.media &&
|
|
currentMediaIndex < product.media.length - 1
|
|
) {
|
|
setCurrentMediaIndex(currentMediaIndex + 1);
|
|
}
|
|
};
|
|
|
|
const prevMedia = () => {
|
|
if (hasMedia && currentMediaIndex > 0) {
|
|
setCurrentMediaIndex(currentMediaIndex - 1);
|
|
}
|
|
};
|
|
|
|
// ============================================
|
|
// 🎨 RENDER
|
|
// ============================================
|
|
return (
|
|
<>
|
|
<div className="modal-overlay" onClick={onClose} />
|
|
<div className="product-modal details-modal">
|
|
{/* Header */}
|
|
<div className="modal-header">
|
|
<div className="header-title-section">
|
|
<Package size={24} className="header-icon" />
|
|
<div>
|
|
<h2>{product.name}</h2>
|
|
<p className="modal-subtitle">
|
|
{getCategoryLabel(product.category)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button className="close-modal" onClick={onClose}>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="modal-content details-content">
|
|
{/* Galerie de médias */}
|
|
{hasMedia && (
|
|
<div className="media-gallery">
|
|
<div className="media-viewer">
|
|
{currentMedia?.type === "image" ? (
|
|
<img
|
|
src={getMediaUrl(currentMedia.url)}
|
|
alt={product.name}
|
|
className="media-display"
|
|
/>
|
|
) : currentMedia?.type === "video" ? (
|
|
<div className="video-container">
|
|
<video
|
|
src={getMediaUrl(currentMedia.url)}
|
|
controls
|
|
className="media-display"
|
|
>
|
|
Votre navigateur ne supporte pas la
|
|
lecture de vidéos.
|
|
</video>
|
|
</div>
|
|
) : (
|
|
<div className="media-placeholder">
|
|
<Package size={64} />
|
|
<p>Aucun média disponible</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
{product.media && product.media.length > 1 && (
|
|
<>
|
|
<button
|
|
className="media-nav prev"
|
|
onClick={prevMedia}
|
|
disabled={currentMediaIndex === 0}
|
|
>
|
|
<ChevronLeft size={24} />
|
|
</button>
|
|
<button
|
|
className="media-nav next"
|
|
onClick={nextMedia}
|
|
disabled={
|
|
currentMediaIndex ===
|
|
product.media.length - 1
|
|
}
|
|
>
|
|
<ChevronRight size={24} />
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{/* Compteur */}
|
|
<div className="media-counter">
|
|
{currentMediaIndex + 1} /{" "}
|
|
{product.media?.length || 0}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Miniatures */}
|
|
{product.media && product.media.length > 1 && (
|
|
<div className="media-thumbnails">
|
|
{product.media.map((media: { url: string; type: string }, index: number) => (
|
|
<button
|
|
key={index}
|
|
className={`thumbnail ${index === currentMediaIndex ? "active" : ""}`}
|
|
onClick={() =>
|
|
setCurrentMediaIndex(index)
|
|
}
|
|
>
|
|
{media.type === "image" ? (
|
|
<img
|
|
src={getMediaUrl(media.url)}
|
|
alt={`Media ${index + 1}`}
|
|
/>
|
|
) : (
|
|
<div className="video-thumb">
|
|
<Play size={20} />
|
|
</div>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Pas de médias */}
|
|
{!hasMedia && (
|
|
<div className="no-media-placeholder">
|
|
<ImageIcon size={64} />
|
|
<p>Aucun média disponible pour ce produit</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Informations détaillées */}
|
|
<div className="details-grid">
|
|
{/* Description */}
|
|
<div className="detail-section full-width">
|
|
<h3 className="section-title">
|
|
<Package size={18} />
|
|
Description
|
|
</h3>
|
|
<p className="detail-description">
|
|
{product.description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stock */}
|
|
<div className="detail-section">
|
|
<h3 className="section-title">
|
|
<Box size={18} />
|
|
Stock Disponible
|
|
</h3>
|
|
<div
|
|
className={`stock-badge ${product.stock > 0 ? "in-stock" : "out-of-stock"}`}
|
|
>
|
|
{product.stock}g
|
|
</div>
|
|
</div>
|
|
|
|
{/* Catégorie */}
|
|
<div className="detail-section">
|
|
<h3 className="section-title">
|
|
<Package size={18} />
|
|
Catégorie
|
|
</h3>
|
|
<div className="category-badge">
|
|
{getCategoryLabel(product.category)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Prix */}
|
|
<div className="detail-section full-width">
|
|
<h3 className="section-title">
|
|
<DollarSign size={18} />
|
|
Tarifs
|
|
</h3>
|
|
<div className="prices-table">
|
|
{product.prices && product.prices.length > 0 ? (
|
|
product.prices.map((price: ProductPrice, index: number) => (
|
|
<div
|
|
key={index}
|
|
className="price-row-display"
|
|
>
|
|
<div className="price-quantity">
|
|
<span className="quantity-value">
|
|
{price.quantity}g
|
|
</span>
|
|
</div>
|
|
<div className="price-arrow">→</div>
|
|
<div className="price-amount">
|
|
<span className="amount-value">
|
|
{price.price.toFixed(2)}€
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="no-data">
|
|
Aucun tarif défini
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Statistiques médias */}
|
|
{hasMedia && product.media && (
|
|
<div className="detail-section full-width">
|
|
<h3 className="section-title">
|
|
<ImageIcon size={18} />
|
|
Médias
|
|
</h3>
|
|
<div className="media-stats">
|
|
<div className="media-stat">
|
|
<ImageIcon size={20} />
|
|
<span>
|
|
{
|
|
product.media.filter(
|
|
(m: { url: string; type: string }) => m.type === "image",
|
|
).length
|
|
}{" "}
|
|
Image
|
|
{product.media.filter(
|
|
(m: { url: string; type: string }) => m.type === "image",
|
|
).length > 1
|
|
? "s"
|
|
: ""}
|
|
</span>
|
|
</div>
|
|
<div className="media-stat">
|
|
<VideoIcon size={20} />
|
|
<span>
|
|
{
|
|
product.media.filter(
|
|
(m: { url: string; type: string }) => m.type === "video",
|
|
).length
|
|
}{" "}
|
|
Vidéo
|
|
{product.media.filter(
|
|
(m: { url: string; type: string }) => m.type === "video",
|
|
).length > 1
|
|
? "s"
|
|
: ""}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Dates */}
|
|
{product.created_at && (
|
|
<div className="detail-section">
|
|
<h3 className="section-title">
|
|
<Calendar size={18} />
|
|
Créé le
|
|
</h3>
|
|
<p className="date-text">
|
|
{formatDate(product.created_at)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{product.updated_at && (
|
|
<div className="detail-section">
|
|
<h3 className="section-title">
|
|
<Calendar size={18} />
|
|
Modifié le
|
|
</h3>
|
|
<p className="date-text">
|
|
{formatDate(product.updated_at)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="modal-actions">
|
|
<button
|
|
type="button"
|
|
className="action-button primary full-width"
|
|
onClick={onClose}
|
|
>
|
|
Fermer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ProductDetailsModal;
|