chore: add ansible backend docker frontend-prep

This commit is contained in:
2026-01-21 13:05:13 +01:00
parent 5a280b6b01
commit 943fe4de7d
14930 changed files with 2341433 additions and 0 deletions
@@ -0,0 +1,300 @@
// ============================================
// 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 } from '../api/api_admin_types'; // ✅ CORRIGÉ
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={`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} />
</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;