chore: update nginx docker compose
This commit is contained in:
@@ -114,14 +114,14 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
|
||||
<div className="media-viewer">
|
||||
{currentMedia?.type === "image" ? (
|
||||
<img
|
||||
src={`http://localhost:8080${currentMedia.url}`}
|
||||
src={`${currentMedia.url}`}
|
||||
alt={product.name}
|
||||
className="media-display"
|
||||
/>
|
||||
) : currentMedia?.type === "video" ? (
|
||||
<div className="video-container">
|
||||
<video
|
||||
src={`http://localhost:8080${currentMedia.url}`}
|
||||
src={`${currentMedia.url}`}
|
||||
controls
|
||||
className="media-display"
|
||||
>
|
||||
@@ -179,7 +179,7 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
|
||||
>
|
||||
{media.type === "image" ? (
|
||||
<img
|
||||
src={`http://localhost:8080${media.url}`}
|
||||
src={`{product.media[0].url}${media.url}`}
|
||||
alt={`Media ${index + 1}`}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -6,457 +6,545 @@
|
||||
// ✅ Upload séparé des médias
|
||||
// ✅ Boutons au lieu de select (comme AdminUsers)
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { X, Plus, Trash2, Upload, DollarSign, Image as ImageIcon } from 'lucide-react';
|
||||
import { updateProductAdmin, deleteProductMediaAdmin, uploadProductMediaAdmin } from '../api/api_admin';
|
||||
import type { Product, ProductPrice } from '../api/api_admin_types';
|
||||
import './ProductModal.css';
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
X,
|
||||
Plus,
|
||||
Trash2,
|
||||
Upload,
|
||||
DollarSign,
|
||||
Image as ImageIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
updateProductAdmin,
|
||||
deleteProductMediaAdmin,
|
||||
uploadProductMediaAdmin,
|
||||
} from "../api/api_admin";
|
||||
import type { Product, ProductPrice } from "../api/api_admin_types";
|
||||
import "./ProductModal.css";
|
||||
|
||||
interface ProductEditModalProps {
|
||||
product: Product;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
product: Product;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, onSuccess }) => {
|
||||
// ============================================
|
||||
// 📝 STATE
|
||||
// ============================================
|
||||
const [name, setName] = useState(product.name);
|
||||
const [category, setCategory] = useState(product.category);
|
||||
const [description, setDescription] = useState(product.description);
|
||||
const [stock, setStock] = useState(product.stock);
|
||||
const [prices, setPrices] = useState<ProductPrice[]>(
|
||||
product.prices && product.prices.length > 0
|
||||
? product.prices
|
||||
: [{ quantity: 1, price: 0 }]
|
||||
);
|
||||
const [existingMedia, setExistingMedia] = useState(product.media || []);
|
||||
const [newMediaFiles, setNewMediaFiles] = useState<File[]>([]);
|
||||
const [mediaToDelete, setMediaToDelete] = useState<number[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const ProductEditModal: React.FC<ProductEditModalProps> = ({
|
||||
product,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}) => {
|
||||
// ============================================
|
||||
// 📝 STATE
|
||||
// ============================================
|
||||
const [name, setName] = useState(product.name);
|
||||
const [category, setCategory] = useState(product.category);
|
||||
const [description, setDescription] = useState(product.description);
|
||||
const [stock, setStock] = useState(product.stock);
|
||||
const [prices, setPrices] = useState<ProductPrice[]>(
|
||||
product.prices && product.prices.length > 0
|
||||
? product.prices
|
||||
: [{ quantity: 1, price: 0 }],
|
||||
);
|
||||
const [existingMedia, setExistingMedia] = useState(product.media || []);
|
||||
const [newMediaFiles, setNewMediaFiles] = useState<File[]>([]);
|
||||
const [mediaToDelete, setMediaToDelete] = useState<number[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// ✅ Options de catégories
|
||||
const categoryOptions = [
|
||||
{ value: 'weed&hash', label: 'Weed & Hash' },
|
||||
{ value: 'zipette&co', label: 'Zipette & Co' },
|
||||
{ value: 'gros&semi', label: 'Gros & Semi' }
|
||||
];
|
||||
// ✅ Options de catégories
|
||||
const categoryOptions = [
|
||||
{ value: "weed&hash", label: "Weed & Hash" },
|
||||
{ value: "zipette&co", label: "Zipette & Co" },
|
||||
{ value: "gros&semi", label: "Gros & Semi" },
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 💰 GESTION DES PRIX
|
||||
// ============================================
|
||||
const addPriceRow = () => {
|
||||
setPrices([...prices, { quantity: 1, price: 0 }]);
|
||||
};
|
||||
// ============================================
|
||||
// 💰 GESTION DES PRIX
|
||||
// ============================================
|
||||
const addPriceRow = () => {
|
||||
setPrices([...prices, { quantity: 1, price: 0 }]);
|
||||
};
|
||||
|
||||
const removePriceRow = (index: number) => {
|
||||
if (prices.length > 1) {
|
||||
setPrices(prices.filter((_, i) => i !== index));
|
||||
}
|
||||
};
|
||||
|
||||
const updatePrice = (index: number, field: 'quantity' | 'price', value: number) => {
|
||||
const newPrices = [...prices];
|
||||
newPrices[index][field] = value;
|
||||
setPrices(newPrices);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 📁 GESTION DES MÉDIAS
|
||||
// ============================================
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
setNewMediaFiles([...newMediaFiles, ...newFiles]);
|
||||
console.log(`📁 ${newFiles.length} nouveau(x) fichier(s) ajouté(s)`);
|
||||
}
|
||||
};
|
||||
|
||||
const removeNewFile = (index: number) => {
|
||||
setNewMediaFiles(newMediaFiles.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const markMediaForDeletion = (mediaId: number) => {
|
||||
setMediaToDelete([...mediaToDelete, mediaId]);
|
||||
setExistingMedia(existingMedia.filter(m => m.id !== mediaId));
|
||||
console.log(`🗑️ Média ${mediaId} marqué pour suppression`);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ✅ VALIDATION
|
||||
// ============================================
|
||||
const validateForm = (): string | null => {
|
||||
if (!name.trim()) return 'Le nom est requis';
|
||||
if (!description.trim()) return 'La description est requise';
|
||||
if (stock < 0) return 'Le stock ne peut pas être négatif';
|
||||
if (prices.length === 0) return 'Au moins un prix est requis';
|
||||
|
||||
for (const price of prices) {
|
||||
if (price.quantity <= 0) return 'Toutes les quantités doivent être positives';
|
||||
if (price.price <= 0) return 'Tous les prix doivent être positifs';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 📤 SOUMISSION - VERSION CORRIGÉE
|
||||
// ============================================
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const validationError = validateForm();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!product.id) {
|
||||
setError('ID du produit manquant');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log('📤 [EDIT_MODAL] Début de la mise à jour du produit:', product.id);
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 1: Supprimer les médias marqués
|
||||
// ============================================
|
||||
if (mediaToDelete.length > 0) {
|
||||
console.log(`🗑️ Suppression de ${mediaToDelete.length} média(s)...`);
|
||||
|
||||
for (const mediaId of mediaToDelete) {
|
||||
try {
|
||||
await deleteProductMediaAdmin(product.id, mediaId);
|
||||
console.log(`✅ Média ${mediaId} supprimé`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Erreur suppression média ${mediaId}:`, err);
|
||||
}
|
||||
const removePriceRow = (index: number) => {
|
||||
if (prices.length > 1) {
|
||||
setPrices(prices.filter((_, i) => i !== index));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 2: Préparer les données de mise à jour (JSON)
|
||||
// ============================================
|
||||
const updateData: Partial<Product> = {
|
||||
name: name.trim(),
|
||||
category,
|
||||
description: description.trim(),
|
||||
stock,
|
||||
prices: prices.map(p => ({
|
||||
quantity: p.quantity,
|
||||
price: p.price
|
||||
}))
|
||||
};
|
||||
const updatePrice = (
|
||||
index: number,
|
||||
field: "quantity" | "price",
|
||||
value: number,
|
||||
) => {
|
||||
const newPrices = [...prices];
|
||||
newPrices[index][field] = value;
|
||||
setPrices(newPrices);
|
||||
};
|
||||
|
||||
console.log('📝 [EDIT_MODAL] Données de mise à jour:', updateData);
|
||||
// ============================================
|
||||
// 📁 GESTION DES MÉDIAS
|
||||
// ============================================
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
setNewMediaFiles([...newMediaFiles, ...newFiles]);
|
||||
console.log(
|
||||
`📁 ${newFiles.length} nouveau(x) fichier(s) ajouté(s)`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 3: Mettre à jour le produit
|
||||
// ============================================
|
||||
const response = await updateProductAdmin(product.id, updateData);
|
||||
const removeNewFile = (index: number) => {
|
||||
setNewMediaFiles(newMediaFiles.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
if (!response.success) {
|
||||
console.error('❌ [EDIT_MODAL] Erreur API:', response.error);
|
||||
setError(response.error || 'Erreur lors de la mise à jour du produit');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const markMediaForDeletion = (mediaId: number) => {
|
||||
setMediaToDelete([...mediaToDelete, mediaId]);
|
||||
setExistingMedia(existingMedia.filter((m) => m.id !== mediaId));
|
||||
console.log(`🗑️ Média ${mediaId} marqué pour suppression`);
|
||||
};
|
||||
|
||||
console.log('✅ [EDIT_MODAL] Produit mis à jour avec succès');
|
||||
// ============================================
|
||||
// ✅ VALIDATION
|
||||
// ============================================
|
||||
const validateForm = (): string | null => {
|
||||
if (!name.trim()) return "Le nom est requis";
|
||||
if (!description.trim()) return "La description est requise";
|
||||
if (stock < 0) return "Le stock ne peut pas être négatif";
|
||||
if (prices.length === 0) return "Au moins un prix est requis";
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 4: Upload des nouveaux médias
|
||||
// ============================================
|
||||
if (newMediaFiles.length > 0) {
|
||||
console.log(`📤 [EDIT_MODAL] Upload de ${newMediaFiles.length} nouveau(x) média(s)...`);
|
||||
|
||||
for (const file of newMediaFiles) {
|
||||
try {
|
||||
const fileType = file.type.startsWith('image/') ? 'image' : 'video';
|
||||
const uploadResponse = await uploadProductMediaAdmin(product.id, file, fileType);
|
||||
|
||||
if (uploadResponse.success) {
|
||||
console.log(`✅ Média uploadé: ${file.name}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Erreur upload ${file.name}:`, uploadResponse.error);
|
||||
for (const price of prices) {
|
||||
if (price.quantity <= 0)
|
||||
return "Toutes les quantités doivent être positives";
|
||||
if (price.price <= 0) return "Tous les prix doivent être positifs";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 📤 SOUMISSION - VERSION CORRIGÉE
|
||||
// ============================================
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const validationError = validateForm();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!product.id) {
|
||||
setError("ID du produit manquant");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log(
|
||||
"📤 [EDIT_MODAL] Début de la mise à jour du produit:",
|
||||
product.id,
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 1: Supprimer les médias marqués
|
||||
// ============================================
|
||||
if (mediaToDelete.length > 0) {
|
||||
console.log(
|
||||
`🗑️ Suppression de ${mediaToDelete.length} média(s)...`,
|
||||
);
|
||||
|
||||
for (const mediaId of mediaToDelete) {
|
||||
try {
|
||||
await deleteProductMediaAdmin(product.id, mediaId);
|
||||
console.log(`✅ Média ${mediaId} supprimé`);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ Erreur suppression média ${mediaId}:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Erreur upload ${file.name}:`, err);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 2: Préparer les données de mise à jour (JSON)
|
||||
// ============================================
|
||||
const updateData: Partial<Product> = {
|
||||
name: name.trim(),
|
||||
category,
|
||||
description: description.trim(),
|
||||
stock,
|
||||
prices: prices.map((p) => ({
|
||||
quantity: p.quantity,
|
||||
price: p.price,
|
||||
})),
|
||||
};
|
||||
|
||||
console.log("📝 [EDIT_MODAL] Données de mise à jour:", updateData);
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 3: Mettre à jour le produit
|
||||
// ============================================
|
||||
const response = await updateProductAdmin(product.id, updateData);
|
||||
|
||||
if (!response.success) {
|
||||
console.error("❌ [EDIT_MODAL] Erreur API:", response.error);
|
||||
setError(
|
||||
response.error ||
|
||||
"Erreur lors de la mise à jour du produit",
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ [EDIT_MODAL] Produit mis à jour avec succès");
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 4: Upload des nouveaux médias
|
||||
// ============================================
|
||||
if (newMediaFiles.length > 0) {
|
||||
console.log(
|
||||
`📤 [EDIT_MODAL] Upload de ${newMediaFiles.length} nouveau(x) média(s)...`,
|
||||
);
|
||||
|
||||
for (const file of newMediaFiles) {
|
||||
try {
|
||||
const fileType = file.type.startsWith("image/")
|
||||
? "image"
|
||||
: "video";
|
||||
const uploadResponse = await uploadProductMediaAdmin(
|
||||
product.id,
|
||||
file,
|
||||
fileType,
|
||||
);
|
||||
|
||||
if (uploadResponse.success) {
|
||||
console.log(`✅ Média uploadé: ${file.name}`);
|
||||
} else {
|
||||
console.warn(
|
||||
`⚠️ Erreur upload ${file.name}:`,
|
||||
uploadResponse.error,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Erreur upload ${file.name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🎉 [EDIT_MODAL] Toutes les modifications appliquées");
|
||||
|
||||
// Fermer et rafraîchir
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error("❌ [EDIT_MODAL] Erreur:", error);
|
||||
setError(
|
||||
error instanceof Error ? error.message : "Erreur inconnue",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('🎉 [EDIT_MODAL] Toutes les modifications appliquées');
|
||||
|
||||
// Fermer et rafraîchir
|
||||
onSuccess();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [EDIT_MODAL] Erreur:', error);
|
||||
setError(error instanceof Error ? error.message : 'Erreur inconnue');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 🎨 RENDER
|
||||
// ============================================
|
||||
return (
|
||||
<>
|
||||
<div className="modal-overlay" onClick={onClose} />
|
||||
<div className="product-modal edit-modal">
|
||||
{/* Header */}
|
||||
<div className="modal-header">
|
||||
<h2>Modifier le Produit</h2>
|
||||
<button className="close-modal" onClick={onClose} disabled={loading}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<form onSubmit={handleSubmit} className="modal-content">
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informations de base */}
|
||||
<div className="form-section">
|
||||
<h3>Informations de Base</h3>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Nom du Produit *</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Ex: OG Kush Premium"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ✅ CATÉGORIE AVEC BOUTONS AU LIEU DE SELECT */}
|
||||
<div className="form-group">
|
||||
<label>Catégorie *</label>
|
||||
<div className="filter-buttons">
|
||||
{categoryOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={category === option.value ? 'active' : ''}
|
||||
onClick={() => setCategory(option.value)}
|
||||
disabled={loading}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description *</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Décrivez le produit en détail..."
|
||||
rows={4}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="stock">Stock (en grammes) *</label>
|
||||
<input
|
||||
id="stock"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={stock}
|
||||
onChange={(e) => setStock(parseFloat(e.target.value) || 0)}
|
||||
placeholder="100"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prix */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<h3>Tarifs</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="add-price-btn"
|
||||
onClick={addPriceRow}
|
||||
disabled={loading}
|
||||
>
|
||||
<Plus size={18} />
|
||||
Ajouter un Prix
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="prices-grid">
|
||||
{prices.map((price, index) => (
|
||||
<div key={index} className="price-row">
|
||||
<div className="price-input-group">
|
||||
<label>Quantité (g)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={price.quantity}
|
||||
onChange={(e) => updatePrice(index, 'quantity', parseInt(e.target.value) || 1)}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="price-input-group">
|
||||
<label>Prix (€)</label>
|
||||
<div className="price-input-wrapper">
|
||||
<DollarSign size={18} />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={price.price}
|
||||
onChange={(e) => updatePrice(index, 'price', parseFloat(e.target.value) || 0)}
|
||||
// ============================================
|
||||
// 🎨 RENDER
|
||||
// ============================================
|
||||
return (
|
||||
<>
|
||||
<div className="modal-overlay" onClick={onClose} />
|
||||
<div className="product-modal edit-modal">
|
||||
{/* Header */}
|
||||
<div className="modal-header">
|
||||
<h2>Modifier le Produit</h2>
|
||||
<button
|
||||
className="close-modal"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{prices.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="remove-price-btn"
|
||||
onClick={() => removePriceRow(index)}
|
||||
disabled={loading}
|
||||
title="Supprimer ce prix"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
<X size={24} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Médias existants */}
|
||||
{existingMedia.length > 0 && (
|
||||
<div className="form-section">
|
||||
<h3>Médias Actuels</h3>
|
||||
<div className="existing-media-grid">
|
||||
{existingMedia.map((media) => (
|
||||
<div key={media.id} className="existing-media-item">
|
||||
{media.type === 'image' ? (
|
||||
<img
|
||||
src={`http://localhost:8080${media.url}`}
|
||||
alt="Media"
|
||||
className="media-thumbnail"
|
||||
/>
|
||||
) : (
|
||||
<div className="video-thumbnail">
|
||||
<ImageIcon size={32} />
|
||||
<span>Vidéo</span>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<form onSubmit={handleSubmit} className="modal-content">
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="delete-media-btn"
|
||||
onClick={() => markMediaForDeletion(media.id!)}
|
||||
disabled={loading}
|
||||
title="Supprimer ce média"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nouveaux médias */}
|
||||
<div className="form-section">
|
||||
<h3>Ajouter de Nouveaux Médias</h3>
|
||||
{/* Informations de base */}
|
||||
<div className="form-section">
|
||||
<h3>Informations de Base</h3>
|
||||
|
||||
<div className="upload-zone">
|
||||
<label htmlFor="media-upload" className="upload-label">
|
||||
<Upload size={32} />
|
||||
<p>Cliquez pour ajouter des fichiers</p>
|
||||
<span>Images (JPG, PNG, WebP) ou Vidéos (MP4, WebM)</span>
|
||||
</label>
|
||||
<input
|
||||
id="media-upload"
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
disabled={loading}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Nom du Produit *</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Ex: OG Kush Premium"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newMediaFiles.length > 0 && (
|
||||
<div className="files-list">
|
||||
{newMediaFiles.map((file, index) => (
|
||||
<div key={index} className="file-item">
|
||||
<div className="file-info">
|
||||
<span className="file-name">{file.name}</span>
|
||||
<span className="file-size">
|
||||
{(file.size / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
{/* ✅ CATÉGORIE AVEC BOUTONS AU LIEU DE SELECT */}
|
||||
<div className="form-group">
|
||||
<label>Catégorie *</label>
|
||||
<div className="filter-buttons">
|
||||
{categoryOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={
|
||||
category === option.value
|
||||
? "active"
|
||||
: ""
|
||||
}
|
||||
onClick={() =>
|
||||
setCategory(option.value)
|
||||
}
|
||||
disabled={loading}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description *</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Décrivez le produit en détail..."
|
||||
rows={4}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="stock">Stock (en grammes) *</label>
|
||||
<input
|
||||
id="stock"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={stock}
|
||||
onChange={(e) =>
|
||||
setStock(parseFloat(e.target.value) || 0)
|
||||
}
|
||||
placeholder="100"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-file-btn"
|
||||
onClick={() => removeNewFile(index)}
|
||||
disabled={loading}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="action-button primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Mise à jour...' : 'Mettre à Jour'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
{/* Prix */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<h3>Tarifs</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="add-price-btn"
|
||||
onClick={addPriceRow}
|
||||
disabled={loading}
|
||||
>
|
||||
<Plus size={18} />
|
||||
Ajouter un Prix
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="prices-grid">
|
||||
{prices.map((price, index) => (
|
||||
<div key={index} className="price-row">
|
||||
<div className="price-input-group">
|
||||
<label>Quantité (g)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={price.quantity}
|
||||
onChange={(e) =>
|
||||
updatePrice(
|
||||
index,
|
||||
"quantity",
|
||||
parseInt(e.target.value) ||
|
||||
1,
|
||||
)
|
||||
}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="price-input-group">
|
||||
<label>Prix (€)</label>
|
||||
<div className="price-input-wrapper">
|
||||
<DollarSign size={18} />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={price.price}
|
||||
onChange={(e) =>
|
||||
updatePrice(
|
||||
index,
|
||||
"price",
|
||||
parseFloat(
|
||||
e.target.value,
|
||||
) || 0,
|
||||
)
|
||||
}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{prices.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="remove-price-btn"
|
||||
onClick={() =>
|
||||
removePriceRow(index)
|
||||
}
|
||||
disabled={loading}
|
||||
title="Supprimer ce prix"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Médias existants */}
|
||||
{existingMedia.length > 0 && (
|
||||
<div className="form-section">
|
||||
<h3>Médias Actuels</h3>
|
||||
<div className="existing-media-grid">
|
||||
{existingMedia.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
className="existing-media-item"
|
||||
>
|
||||
{media.type === "image" ? (
|
||||
<img
|
||||
src={`${media.url}`}
|
||||
alt="Media"
|
||||
className="media-thumbnail"
|
||||
/>
|
||||
) : (
|
||||
<div className="video-thumbnail">
|
||||
<ImageIcon size={32} />
|
||||
<span>Vidéo</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="delete-media-btn"
|
||||
onClick={() =>
|
||||
markMediaForDeletion(media.id!)
|
||||
}
|
||||
disabled={loading}
|
||||
title="Supprimer ce média"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nouveaux médias */}
|
||||
<div className="form-section">
|
||||
<h3>Ajouter de Nouveaux Médias</h3>
|
||||
|
||||
<div className="upload-zone">
|
||||
<label
|
||||
htmlFor="media-upload"
|
||||
className="upload-label"
|
||||
>
|
||||
<Upload size={32} />
|
||||
<p>Cliquez pour ajouter des fichiers</p>
|
||||
<span>
|
||||
Images (JPG, PNG, WebP) ou Vidéos (MP4,
|
||||
WebM)
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="media-upload"
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
disabled={loading}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newMediaFiles.length > 0 && (
|
||||
<div className="files-list">
|
||||
{newMediaFiles.map((file, index) => (
|
||||
<div key={index} className="file-item">
|
||||
<div className="file-info">
|
||||
<span className="file-name">
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="file-size">
|
||||
{(file.size / 1024).toFixed(1)}{" "}
|
||||
KB
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-file-btn"
|
||||
onClick={() => removeNewFile(index)}
|
||||
disabled={loading}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="action-button primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Mise à jour..." : "Mettre à Jour"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductEditModal;
|
||||
export default ProductEditModal;
|
||||
|
||||
@@ -514,7 +514,7 @@ const AdminProduct: React.FC = () => {
|
||||
product.media.length > 0 ? (
|
||||
<div className="product-image">
|
||||
<img
|
||||
src={`http://localhost:8080${product.media[0].url}`}
|
||||
src={`${product.media[0].url}`}
|
||||
alt={
|
||||
product.name
|
||||
}
|
||||
|
||||
@@ -85,9 +85,7 @@ function UserAccueil() {
|
||||
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||
);
|
||||
|
||||
return videoMedia?.url
|
||||
? `http://localhost:8080${videoMedia.url}`
|
||||
: undefined;
|
||||
return videoMedia?.url ? `${videoMedia.url}` : undefined;
|
||||
};
|
||||
const getProductImage = (product: Product): string => {
|
||||
// Pas de media ? Image placeholder
|
||||
@@ -101,7 +99,7 @@ function UserAccueil() {
|
||||
|
||||
// Vérifier si c'est un objet avec type "image" et url
|
||||
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
||||
return `http://localhost:8080${mediaItem.url}`;
|
||||
return `${mediaItem.url}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ function Cart() {
|
||||
for (let i = 0; i < product.media.length; i++) {
|
||||
const mediaItem = product.media[i];
|
||||
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
||||
return `http://localhost:8080${mediaItem.url}`;
|
||||
return `${mediaItem.url}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,9 +136,7 @@ function Cart() {
|
||||
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||
);
|
||||
|
||||
return videoMedia?.url
|
||||
? `http://localhost:8080${videoMedia.url}`
|
||||
: undefined;
|
||||
return videoMedia?.url ? `${videoMedia.url}` : undefined;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -117,7 +117,7 @@ function OrderDetails() {
|
||||
for (let i = 0; i < product.media.length; i++) {
|
||||
const mediaItem = product.media[i];
|
||||
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
|
||||
return `http://localhost:8080${mediaItem.url}`;
|
||||
return `${mediaItem.url}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +142,7 @@ function OrderDetails() {
|
||||
(mediaItem) => mediaItem && mediaItem.type === "video",
|
||||
);
|
||||
|
||||
return videoMedia?.url
|
||||
? `http://localhost:8080${videoMedia.url}`
|
||||
: undefined;
|
||||
return videoMedia?.url ? `${videoMedia.url}` : undefined;
|
||||
};
|
||||
|
||||
// HANDLERS VIDÉO
|
||||
|
||||
Reference in New Issue
Block a user