chore: add create user route
This commit is contained in:
@@ -5,296 +5,358 @@
|
||||
// ✅ 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 } from '../api/api_admin_types'; // ✅ CORRIGÉ
|
||||
import './ProductModal.css';
|
||||
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 } from "../api/api_admin_types"; // ✅ CORRIGÉ
|
||||
import "./ProductModal.css";
|
||||
|
||||
interface ProductDetailsModalProps {
|
||||
product: Product;
|
||||
onClose: () => void;
|
||||
product: Product;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({ product, onClose }) => {
|
||||
// ============================================
|
||||
// 📝 STATE
|
||||
// ============================================
|
||||
const [currentMediaIndex, setCurrentMediaIndex] = useState(0);
|
||||
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;
|
||||
}
|
||||
};
|
||||
// ============================================
|
||||
// 🎨 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 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 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 nextMedia = () => {
|
||||
if (
|
||||
hasMedia &&
|
||||
product.media &&
|
||||
currentMediaIndex < product.media.length - 1
|
||||
) {
|
||||
setCurrentMediaIndex(currentMediaIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevMedia = () => {
|
||||
if (hasMedia && currentMediaIndex > 0) {
|
||||
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={`http://localhost:8080${currentMedia.url}`}
|
||||
alt={product.name}
|
||||
className="media-display"
|
||||
/>
|
||||
) : currentMedia?.type === 'video' ? (
|
||||
<div className="video-container">
|
||||
<video
|
||||
src={`http://localhost:8080${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, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`thumbnail ${index === currentMediaIndex ? 'active' : ''}`}
|
||||
onClick={() => setCurrentMediaIndex(index)}
|
||||
>
|
||||
{media.type === 'image' ? (
|
||||
<img
|
||||
src={`http://localhost:8080${media.url}`}
|
||||
alt={`Media ${index + 1}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="video-thumb">
|
||||
<Play size={20} />
|
||||
// ============================================
|
||||
// 🎨 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>
|
||||
)}
|
||||
</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, index) => (
|
||||
<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 => m.type === 'image').length} Image{product.media.filter(m => m.type === 'image').length > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div className="media-stat">
|
||||
<VideoIcon size={20} />
|
||||
<span>{product.media.filter(m => m.type === 'video').length} Vidéo{product.media.filter(m => m.type === 'video').length > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<button className="close-modal" onClick={onClose}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</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>
|
||||
)}
|
||||
{/* 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={`http://localhost:8080${currentMedia.url}`}
|
||||
alt={product.name}
|
||||
className="media-display"
|
||||
/>
|
||||
) : currentMedia?.type === "video" ? (
|
||||
<div className="video-container">
|
||||
<video
|
||||
src={`http://localhost:8080${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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
{/* 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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button primary full-width"
|
||||
onClick={onClose}
|
||||
>
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
{/* 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, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`thumbnail ${index === currentMediaIndex ? "active" : ""}`}
|
||||
onClick={() =>
|
||||
setCurrentMediaIndex(index)
|
||||
}
|
||||
>
|
||||
{media.type === "image" ? (
|
||||
<img
|
||||
src={`http://localhost:8080${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, index) => (
|
||||
<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) => m.type === "image",
|
||||
).length
|
||||
}{" "}
|
||||
Image
|
||||
{product.media.filter(
|
||||
(m) => m.type === "image",
|
||||
).length > 1
|
||||
? "s"
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="media-stat">
|
||||
<VideoIcon size={20} />
|
||||
<span>
|
||||
{
|
||||
product.media.filter(
|
||||
(m) => m.type === "video",
|
||||
).length
|
||||
}{" "}
|
||||
Vidéo
|
||||
{product.media.filter(
|
||||
(m) => 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;
|
||||
export default ProductDetailsModal;
|
||||
|
||||
Reference in New Issue
Block a user