Files
projet_gestion_commande/frontend-prep/src/components/ProductDetailsModal.tsx
T
2026-03-05 19:09:16 +01:00

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;