import React, { useState, useEffect, useCallback, useRef } from 'react';
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInAnonymously, // Utilisé pour l'accès transparent des clients
onAuthStateChanged,
signInWithEmailAndPassword,
createUserWithEmailAndPassword, // Gardé pour l'admin si besoin de créer un nouvel admin via l'interface
signOut
} from 'firebase/auth';
import {
getFirestore,
collection,
query,
onSnapshot,
addDoc,
doc,
updateDoc,
deleteDoc,
getDoc,
where,
getDocs,
} from 'firebase/firestore';
import { getStorage, ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage';
// --- Configuration Firebase (Personnalisée avec vos informations) ---
// Ces variables sont injectées au moment de l'exécution par l'environnement Canvas.
// POUR UN DÉPLOIEMENT RÉEL, UTILISEZ DES VARIABLES D'ENVIRONNEMENT (.env.local)
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
// ✅ Votre configuration Firebase spécifique
const firebaseConfig = {
apiKey: "AIzaSyCnJ1TntLmF5KMzW32G3Ye2WMUcpHRTdHM",
authDomain: "mamadou-66fe8.firebaseapp.com",
projectId: "mamadou-66fe8",
storageBucket: "mamadou-66fe8.appspot.com",
messagingSenderId: "435105859506",
appId: "1:435105859506:web:9a04e4125a25d3030ac6ab",
measurementId: "G-CN4645RGWC"
};
// ✅ L'email de l'administrateur que vous avez fourni
const ADMIN_EMAIL = 'checksawadogo305@gmail.com';
// ✅ Votre numéro WhatsApp professionnel
const WHATSAPP_NUMBER = '+2250564574378'; // Format international avec code pays
// Initialisation de Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
const storage = getStorage(app); // Initialisation de Firebase Storage
// --- Composant Toast Notification ---
const ToastNotification = React.memo(({ message, type, onClose }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, 5000); // Disparaît après 5 secondes
return () => clearTimeout(timer);
}, [onClose]);
const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500';
const textColor = 'text-white';
return (
);
});
// --- Composant Modal de Confirmation Personnalisée ---
const ConfirmationModal = React.memo(({ message, onConfirm, onCancel }) => {
const modalRef = useRef(null);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onCancel();
}
};
// Piège le focus à l'intérieur de la modale pour l'accessibilité
if (modalRef.current) {
modalRef.current.focus();
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [onCancel]);
return (
Confirmation
{message}
);
});
// --- Composant principal de l'application ---
function App() {
// États pour gérer les données et l'interface utilisateur
const [products, setProducts] = useState([]);
const [orders, setOrders] = useState([]); // Vue de l'admin de toutes les commandes
const [userOrders, setUserOrders] = useState([]); // Vue de l'utilisateur de ses propres commandes
const [reviews, setReviews] = useState([]); // Vue de l'admin de tous les avis
const [selectedProduct, setSelectedProduct] = useState(null);
const [isAdmin, setIsAdmin] = useState(false);
const [currentUser, setCurrentUser] = useState(null); // Utilisateur actuellement connecté (peut être anonyme)
// REMOVED: showAuthModal, isLoginMode, authEmail, authPassword states
const [adminAuthModal, setAdminAuthModal] = useState(false); // Pour la modal de connexion admin
// L'email et le mot de passe de l'admin sont gérés localement dans AdminAuthModal maintenant
// const [adminEmail, setAdminEmail] = useState('');
// const [adminPassword, setAdminPassword] = useState('');
const [loading, setLoading] = useState(true);
const [toast, setToast] = useState(null); // Pour les notifications toast
const [activeAdminTab, setActiveAdminTab] = useState('products');
// NOUVEAU : État pour le panier d'achat
const [cart, setCart] = useState(() => {
try {
const savedCart = localStorage.getItem('mamadou_electronique_cart');
return savedCart ? JSON.parse(savedCart) : [];
} catch (error) {
console.error("Erreur de récupération du panier depuis localStorage:", error);
return [];
}
});
const [showCartModal, setShowCartModal] = useState(false);
// États pour la modal de confirmation
const [showConfirmation, setShowConfirmation] = useState(false);
const [confirmationMessage, setConfirmationMessage] = useState('');
const [confirmationAction, setConfirmationAction] = useState(null);
// États pour la recherche et le filtrage côté client
const [searchTerm, setSearchTerm] = useState('');
const [filterInStock, setFilterInStock] = useState(false);
const [filterPromotion, setFilterPromotion] = useState(false);
const [sortBy, setSortBy] = useState('name'); // 'name', 'priceAsc', 'priceDesc'
// Effet pour persister le panier dans localStorage
useEffect(() => {
try {
localStorage.setItem('mamadou_electronique_cart', JSON.stringify(cart));
} catch (error) {
console.error("Erreur de sauvegarde du panier dans localStorage:", error);
}
}, [cart]);
// Fonction pour afficher une notification toast
const showToast = useCallback((message, type = 'success') => {
setToast({ message, type });
}, []);
// Fonction pour afficher la modal de confirmation
const confirmAction = useCallback((msg, action) => {
setConfirmationMessage(msg);
setConfirmationAction(() => action);
setShowConfirmation(true);
}, []);
// Fonctions de gestion du panier
const addToCart = useCallback((product) => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
showToast(`${product.name} (x${existingItem.quantity + 1}) dans le panier !`, 'success');
return prevCart.map(item =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
);
} else {
showToast(`${product.name} ajouté au panier !`, 'success');
return [...prevCart, { ...product, quantity: 1 }];
}
});
}, [showToast]);
const removeFromCart = useCallback((productId) => {
setCart(prevCart => prevCart.filter(item => item.id !== productId));
showToast('Article retiré du panier.', 'success');
}, [showToast]);
const updateCartQuantity = useCallback((productId, quantity) => {
setCart(prevCart => {
if (quantity <= 0) {
return prevCart.filter(item => item.id !== productId);
}
return prevCart.map(item =>
item.id === productId ? { ...item, quantity: quantity } : item
);
});
}, []);
const clearCart = useCallback(() => {
setCart([]);
showToast('Panier vidé.', 'success');
}, [showToast]);
// Fonction pour générer un ID de commande unique
const generateOrderId = () => {
const datePart = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase(); // 6 random chars
return `CMD-${datePart}-${randomPart}`;
};
// Fonction pour passer la commande (simulé)
const placeOrder = useCallback(async (customerName, customerContact) => {
if (cart.length === 0) {
showToast('Votre panier est vide.', 'error');
return;
}
if (!currentUser) {
showToast('Erreur: Utilisateur non identifié. Veuillez rafraîchir la page.', 'error');
return;
}
setLoading(true);
try {
const orderId = generateOrderId();
const orderData = {
orderId: orderId,
userId: currentUser.uid, // Utilise l'UID de l'utilisateur anonyme ou authentifié
customerName,
customerContact,
items: cart.map(item => ({
productId: item.id,
name: item.name,
price: item.price,
quantity: item.quantity,
imageUrl: item.imageUrl,
})),
total: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
orderDate: new Date().toISOString(),
status: 'En attente', // Statut de la commande
};
// Enregistrer la commande dans la collection de commandes de l'utilisateur
await addDoc(collection(db, `artifacts/${appId}/users/${currentUser.uid}/orders`), orderData);
// Enregistrer une copie de la commande dans la collection globale pour l'admin
await addDoc(collection(db, `artifacts/${appId}/public/data/all_orders`), orderData);
const orderDetails = cart.map(item =>
`* ${item.name} (x${item.quantity}) - ${item.price * item.quantity} FCFA`
).join('\n');
const whatsappMessage = encodeURIComponent(
`Bonjour, je souhaite passer la commande suivante (ID: ${orderId}) :
* Client: ${customerName}
* Contact: ${customerContact}
--- Détails de la commande ---
${orderDetails}
* Total de la commande: ${orderData.total} FCFA
Merci de confirmer la disponibilité et les modalités de livraison.`
);
const whatsappLink = `https://wa.me/${WHATSAPP_NUMBER}?text=${whatsappMessage}`;
window.open(whatsappLink, '_blank');
clearCart();
showToast(`Commande ${orderId} passée avec succès !`, 'success');
setShowCartModal(false);
} catch (error) {
console.error('Erreur lors de la commande:', error);
showToast('Erreur lors de la commande. Veuillez réessayer.', 'error');
} finally {
setLoading(false);
}
}, [cart, currentUser, showToast, clearCart]);
// Fonction de connexion pour l'administrateur
const handleAdminLogin = useCallback(async (email, password) => { // Reçoit email et password en arguments
setToast(null);
setLoading(true);
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password); // Utilise les arguments
if (userCredential.user.email === ADMIN_EMAIL) {
showToast('Connexion administrateur réussie !', 'success');
setAdminAuthModal(false);
// Pas besoin de réinitialiser adminEmail/Password ici, ils sont gérés localement dans la modal
} else {
await signOut(auth); // Déconnecte si ce n'est pas l'admin
showToast('Accès refusé: Cet email n\'est pas un compte administrateur.', 'error');
}
} catch (error) {
console.error('Erreur d\'authentification admin:', error.code, error.message);
let errorMessage = 'Erreur d\'authentification. Veuillez réessayer.';
if (error.code === 'auth/user-not-found' || error.code === 'auth/wrong-password' || error.code === 'auth/invalid-credential') {
errorMessage = 'Email ou mot de passe incorrect.';
} else if (error.code === 'auth/invalid-email') {
errorMessage = 'Format d\'email invalide.';
}
showToast(errorMessage, 'error');
} finally {
setLoading(false);
}
}, [showToast]); // showToast est la seule dépendance externe
const handleLogout = async () => {
try {
await signOut(auth);
// Après la déconnexion, réauthentifier anonymement pour les clients
await signInAnonymously(auth);
showToast('Déconnexion réussie.', 'success');
} catch (error) {
console.error('Erreur de déconnexion:', error);
showToast('Erreur lors de la déconnexion. Veuillez réessayer.', 'error');
}
};
// Fonction pour fermer la modale d'authentification admin
const handleCloseAdminAuthModal = useCallback(() => setAdminAuthModal(false), []);
// Effet pour l'authentification Firebase et l'écoute des données
useEffect(() => {
const unsubscribeAuth = onAuthStateChanged(auth, async (user) => {
if (user) {
setCurrentUser(user);
setIsAdmin(user.email === ADMIN_EMAIL);
console.log('Utilisateur authentifié:', user.uid, 'Est admin:', user.email === ADMIN_EMAIL);
// Écoute des commandes de l'utilisateur actuel (même anonyme)
const userOrdersCollectionRef = collection(db, `artifacts/${appId}/users/${user.uid}/orders`);
const unsubscribeUserOrders = onSnapshot(userOrdersCollectionRef, (snapshot) => {
const ordersData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
ordersData.sort((a, b) => new Date(b.orderDate) - new Date(a.orderDate));
setUserOrders(ordersData);
console.log('Commandes utilisateur mises à jour:', ordersData);
}, (error) => {
console.error('Erreur lors de la récupération des commandes utilisateur:', error);
showToast('Erreur lors du chargement de vos commandes. Veuillez réessayer.', 'error');
});
// Si l'utilisateur est admin, écouter toutes les commandes et tous les avis
if (user.email === ADMIN_EMAIL) {
const allOrdersCollectionRef = collection(db, `artifacts/${appId}/public/data/all_orders`);
const unsubscribeAllOrders = onSnapshot(allOrdersCollectionRef, (snapshot) => {
const ordersData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
ordersData.sort((a, b) => new Date(b.orderDate) - new Date(a.orderDate));
setOrders(ordersData);
console.log('Toutes les commandes mises à jour (Admin):', ordersData);
}, (error) => {
console.error('Erreur lors de la récupération de toutes les commandes:', error);
showToast('Erreur lors du chargement des commandes admin. Veuillez réessayer.', 'error');
});
const reviewsCollectionRef = collection(db, `artifacts/${appId}/public/data/reviews`);
const unsubscribeReviews = onSnapshot(reviewsCollectionRef, (snapshot) => {
const reviewsData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setReviews(reviewsData);
console.log('Avis mis à jour (Admin):', reviewsData);
}, (error) => {
console.error('Erreur lors de la récupération des avis:', error);
showToast('Erreur lors du chargement des avis. Veuillez réessayer.', 'error');
});
return () => {
unsubscribeUserOrders();
unsubscribeAllOrders();
unsubscribeReviews();
};
} else {
// Si un utilisateur non-admin se connecte (ex: après déconnexion de l'admin),
// s'assurer qu'il est anonyme pour ne pas avoir de données d'admin.
// Si l'utilisateur est déjà anonyme, cela ne fait rien.
if (!user.isAnonymous) {
console.log('Déconnexion de l\'utilisateur non-admin et tentative de connexion anonyme.');
await signOut(auth);
await signInAnonymously(auth);
}
return () => {
unsubscribeUserOrders();
};
}
} else {
// Si aucun utilisateur n'est trouvé (y compris après déconnexion ou au premier chargement),
// se connecter anonymement pour permettre l'accès aux fonctionnalités client.
console.log('Aucun utilisateur trouvé. Tentative de connexion anonyme...');
try {
await signInAnonymously(auth);
console.log('Connecté anonymement pour l\'accès public.');
} catch (error) {
console.error('Erreur d\'authentification Firebase (anonyme):', error);
// ✅ Message d'erreur plus spécifique pour guider l'utilisateur
showToast('Erreur de connexion au service anonyme. Veuillez réessayer. Assurez-vous que l\'authentification anonyme est activée dans votre console Firebase.', 'error');
}
setCurrentUser(null);
setIsAdmin(false);
setUserOrders([]);
setOrders([]); // Effacer les commandes admin si non admin
setReviews([]); // Effacer les avis si non admin
}
setLoading(false);
});
const productsCollectionRef = collection(db, `artifacts/${appId}/public/data/products`);
const unsubscribeProducts = onSnapshot(productsCollectionRef, async (snapshot) => {
const productsData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
const productsWithRatings = await Promise.all(productsData.map(async (product) => {
const productReviewsQuery = query(collection(db, `artifacts/${appId}/public/data/reviews`), where('productId', '==', product.id), where('approved', '==', true));
const reviewSnapshot = await getDocs(productReviewsQuery);
const productReviews = reviewSnapshot.docs.map(doc => doc.data());
const totalRating = productReviews.reduce((sum, review) => sum + review.rating, 0);
const averageRating = productReviews.length > 0 ? (totalRating / productReviews.length) : 0;
const reviewCount = productReviews.length;
return { ...product, averageRating, reviewCount };
}));
setProducts(productsWithRatings);
console.log('Produits mis à jour avec ratings:', productsWithRatings);
}, (error) => {
console.error('Erreur lors de la récupération des produits:', error);
showToast('Erreur lors du chargement des produits. Veuillez réessayer.', 'error');
});
return () => {
unsubscribeAuth();
unsubscribeProducts();
};
}, [confirmAction, showToast]);
// Logique de filtrage et de tri des produits pour l'affichage client
const filteredAndSortedProducts = products
.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.descriptionCourte.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.descriptionComplete.toLowerCase().includes(searchTerm.toLowerCase());
const matchesInStock = !filterInStock || product.inStock;
const matchesPromotion = !filterPromotion || product.isPromotion;
return matchesSearch && matchesInStock && matchesPromotion;
})
.sort((a, b) => {
if (sortBy === 'priceAsc') {
return a.price - b.price;
} else if (sortBy === 'priceDesc') {
return b.price - a.price;
} else { // Default to 'name'
return a.name.localeCompare(b.name);
}
});
// Composant de carte de produit pour l'affichage client
const ProductCard = React.memo(({ product, onClick, onAddToCart }) => (
{product.isPromotion && (
PROMO
)}

{
console.error("Erreur de chargement d'image pour le produit:", product.name, "URL:", product.imageUrl, e);
e.target.onerror = null;
e.target.src = `https://placehold.co/150x150/E0F2F7/000000?text=Image%20Non%20Dispo`;
}}
loading="lazy"
/>
{product.name}
{product.descriptionCourte}
{product.price} FCFA
{product.averageRating > 0 && (
{'⭐'.repeat(Math.round(product.averageRating))} ({product.reviewCount})
)}
{product.inStock ? (
En stock
) : (
Épuisé
)}
{product.inStock && (
)}
));
// Composant de détail de produit pour l'affichage client
const ProductDetail = React.memo(({ product, onClose, onAddToCart, currentUser, showToast }) => {
const modalRef = useRef(null);
const [reviewText, setReviewText] = useState('');
const [reviewRating, setReviewRating] = useState(0);
const [submittingReview, setSubmittingReview] = useState(false);
const [productReviews, setProductReviews] = useState([]);
const [reviewSummary, setReviewSummary] = useState(''); // Pour le résumé des avis
const [summarizingReviews, setSummarizingReviews] = useState(false); // État de chargement du résumé
// Fetch reviews for this product
useEffect(() => {
const q = query(collection(db, `artifacts/${appId}/public/data/reviews`), where('productId', '==', product.id), where('approved', '==', true));
const unsubscribeReviews = onSnapshot(q, (snapshot) => {
const reviewsData = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setProductReviews(reviewsData);
}, (error) => {
console.error("Erreur de récupération des avis pour le produit:", error);
});
return () => unsubscribeReviews();
}, [product.id, showToast]);
// Fonction pour résumer les avis avec Gemini
const summarizeReviewsWithGemini = async () => {
setSummarizingReviews(true);
setReviewSummary('');
try {
if (productReviews.length === 0) {
showToast('Aucun avis à résumer.', 'info');
setSummarizingReviews(false);
return;
}
const reviewsContent = productReviews.map(r => `"${r.text}" (Note: ${r.rating}/5)`).join('\n');
const prompt = `Veuillez résumer les avis suivants sur un produit. Mettez en évidence les points positifs et négatifs principaux. Voici les avis :\n\n${reviewsContent}`;
const apiKey = ""; // Géré par l'environnement Canvas
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const chatHistory = [{ role: "user", parts: [{ text: prompt }] }];
const payload = { contents: chatHistory };
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
const text = result.candidates[0].content.parts[0].text;
setReviewSummary(text);
showToast('Résumé généré avec succès !', 'success');
} else {
showToast('Erreur lors de la génération du résumé des avis.', 'error');
console.error('Réponse Gemini API inattendue pour le résumé:', result);
}
} catch (error) {
showToast(`Erreur lors de la génération du résumé: ${error.message}`, 'error');
console.error('Erreur lors de l\'appel à Gemini pour le résumé:', error);
} finally {
setSummarizingReviews(false);
}
};
// JSON-LD Schema Markup pour le SEO
const productSchema = {
"@context": "https://schema.org/",
"@type": "Product",
"name": product.name,
"image": product.imageUrl,
"description": product.descriptionComplete,
"sku": product.id,
"offers": {
"@type": "Offer",
"url": window.location.href,
"priceCurrency": "XOF",
"price": product.price,
"itemCondition": "https://schema.org/NewCondition",
"availability": product.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
},
"aggregateRating": product.averageRating > 0 ? {
"@type": "AggregateRating",
"ratingValue": product.averageRating.toFixed(1),
"reviewCount": product.reviewCount
} : undefined,
"review": productReviews.length > 0 ? productReviews.map(review => ({
"@type": "Review",
"reviewRating": {
"@type": "Rating",
"ratingValue": review.rating
},
"author": { "@type": "Person", "name": review.userName || "Anonyme" },
"reviewBody": review.text
})) : undefined
};
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
if (modalRef.current) {
modalRef.current.focus();
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
useEffect(() => {
const script = document.createElement('script');
script.type = 'application/ld+json';
script.innerHTML = JSON.stringify(productSchema);
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, [productSchema]);
const handleReviewSubmit = async (e) => {
e.preventDefault();
if (!currentUser) {
showToast('Erreur: Impossible de soumettre l\'avis sans ID utilisateur.', 'error');
return;
}
if (reviewRating === 0 || reviewText.trim() === '') {
showToast('Veuillez donner une note et écrire un avis.', 'error');
return;
}
setSubmittingReview(true);
try {
await addDoc(collection(db, `artifacts/${appId}/public/data/reviews`), {
productId: product.id,
userId: currentUser.uid, // Utilise l'UID de l'utilisateur anonyme
userName: currentUser.isAnonymous ? 'Client Anonyme' : (currentUser.email || 'Utilisateur'), // Nom pour l'avis
rating: reviewRating,
text: reviewText.trim(),
createdAt: new Date().toISOString(),
approved: false,
});
showToast('Votre avis a été soumis et est en attente d\'approbation.', 'success');
setReviewText('');
setReviewRating(0);
} catch (error) {
console.error('Erreur lors de la soumission de l\'avis:', error);
showToast('Erreur lors de la soumission de l\'avis. Veuillez réessayer.', 'error');
} finally {
setSubmittingReview(false);
}
};
return (

{
console.error("Erreur de chargement d'image pour le produit:", product.name, "URL:", product.imageUrl, e);
e.target.onerror = null;
e.target.src = `https://placehold.co/300x300/E0F2F7/000000?text=Image%20Non%20Dispo`;
}}
loading="lazy"
/>
{product.name}
{product.descriptionComplete}
{product.price} FCFA
{product.averageRating > 0 && (
{'⭐'.repeat(Math.round(product.averageRating))} ({product.reviewCount} avis)
)}
{product.inStock ? (
En stock
) : (
Épuisé
)}
{product.isPromotion && (
Promotion !
)}
{product.inStock && (
)}
{/* Section Avis Clients */}
Avis des clients ({productReviews.length})
{productReviews.length === 0 ? (
Soyez le premier à laisser un avis !
) : (
<>
{productReviews.length > 0 && (
)}
{reviewSummary && (
Résumé des Avis :
{reviewSummary}
)}
>
)}
{/* Formulaire de soumission d'avis */}
);
});
// Composant d'ajout/modification de produit pour l'admin
const ProductForm = React.memo(({ productToEdit, onSave, onCancel, showToast }) => {
const [name, setName] = useState(productToEdit?.name || '');
const [descriptionCourte, setDescriptionCourte] = useState(productToEdit?.descriptionCourte || '');
const [descriptionComplete, setDescriptionComplete] = useState(productToEdit?.descriptionComplete || '');
const [price, setPrice] = useState(productToEdit?.price || '');
const [imageUrl, setImageUrl] = useState(productToEdit?.imageUrl || '');
const [imageFile, setImageFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const [inStock, setInStock] = useState(productToEdit?.inStock ?? true);
const [isPromotion, setIsPromotion] = useState(productToEdit?.isPromotion ?? false);
const [reviewsAdminText, setReviewsAdminText] = useState(productToEdit?.reviews?.join('\n') || '');
const [formError, setFormError] = useState('');
const [generatingDescription, setGeneratingDescription] = useState(false); // État de chargement de la génération
const formRef = useRef(null);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onCancel();
}
};
if (formRef.current) {
formRef.current.focus();
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [onCancel]);
const handleFileChange = (e) => {
if (e.target.files[0]) {
setImageFile(e.target.files[0]);
setFormError('');
}
};
const uploadImage = async (file) => {
if (!file) return imageUrl;
setIsUploading(true);
setUploadProgress(0);
const storageRef = ref(storage, `product_images/${Date.now()}_${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
return new Promise((resolve, reject) => {
uploadTask.on('state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
setUploadProgress(progress);
},
(error) => {
console.error("Erreur de téléchargement d'image:", error);
showToast("Erreur lors du téléchargement de l'image.", 'error');
setIsUploading(false);
reject(error);
},
async () => {
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
setIsUploading(false);
resolve(downloadURL);
}
);
});
};
// Fonction pour générer la description complète avec Gemini
const generateFullDescriptionWithGemini = async () => {
setGeneratingDescription(true);
try {
if (!descriptionCourte.trim()) {
showToast('Veuillez saisir une description courte pour générer la description complète.', 'info');
setGeneratingDescription(false);
return;
}
const prompt = `Développez cette courte description de produit en une description complète et attrayante pour le commerce électronique, en mettant en évidence les principales caractéristiques, les avantages et un appel à l'action. Le produit est : "${descriptionCourte}"`;
const apiKey = ""; // Géré par l'environnement Canvas
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
const chatHistory = [{ role: "user", parts: [{ text: prompt }] }];
const payload = { contents: chatHistory };
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
const text = result.candidates[0].content.parts[0].text;
setDescriptionComplete(text);
showToast('Description générée avec succès !', 'success');
} else {
showToast('Erreur lors de la génération de la description complète.', 'error');
console.error('Réponse Gemini API inattendue pour la description:', result);
}
} catch (error) {
showToast(`Erreur lors de la génération de la description: ${error.message}`, 'error');
console.error('Erreur lors de l\'appel à Gemini pour la description:', error);
} finally {
setGeneratingDescription(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setFormError('');
if (!name || !descriptionCourte || !descriptionComplete || !price) {
setFormError('Tous les champs obligatoires doivent être remplis.');
return;
}
if (isNaN(parseFloat(price)) || parseFloat(price) <= 0) {
setFormError('Le prix doit être un nombre positif.');
return;
}
let finalImageUrl = imageUrl;
if (imageFile) {
try {
finalImageUrl = await uploadImage(imageFile);
} catch (uploadError) {
setFormError("Échec du téléchargement de l'image. Veuillez réessayer.");
return;
}
} else if (imageUrl && (!typeof imageUrl === 'string' || !imageUrl.startsWith('http'))) {
setFormError("L'URL de l'image doit être vide ou commencer par 'http'.");
return;
}
const productData = {
name,
descriptionCourte,
descriptionComplete,
price: parseFloat(price),
imageUrl: finalImageUrl,
inStock,
isPromotion,
reviews: reviewsAdminText.split('\n').map(r => r.trim()).filter(r => r !== ''),
createdAt: productToEdit ? productToEdit.createdAt : new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
try {
if (productToEdit) {
const productRef = doc(db, `artifacts/${appId}/public/data/products`, productToEdit.id);
await updateDoc(productRef, productData);
showToast('Produit mis à jour avec succès !', 'success');
} else {
await addDoc(collection(db, `artifacts/${appId}/public/data/products`), productData);
showToast('Produit ajouté avec succès !', 'success');
}
onSave();
} catch (error) {
console.error('Erreur lors de la sauvegarde du produit:', error);
showToast('Erreur lors de la sauvegarde du produit. Veuillez réessayer.', 'error');
}
};
return (
);
});
// Composant d'ajout/modification de commande pour l'admin
const OrderForm = React.memo(({ orderToEdit, onSave, onCancel }) => {
const [productName, setProductName] = useState(orderToEdit?.productName || '');
const [customerName, setCustomerName] = useState(orderToEdit?.customerName || '');
const [customerContact, setCustomerContact] = useState(orderToEdit?.customerContact || '');
const [orderDate, setOrderDate] = useState(orderToEdit?.orderDate ? new Date(orderToEdit.orderDate).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]);
const [status, setStatus] = useState(orderToEdit?.status || 'En attente');
const [formError, setFormError] = useState('');
const formRef = useRef(null);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onCancel();
}
};
if (formRef.current) {
formRef.current.focus();
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [onCancel]);
const handleSubmit = async (e) => {
e.preventDefault();
setFormError('');
if (!productName || !customerName || !customerContact || !orderDate || !status) {
setFormError('Tous les champs obligatoires doivent être remplis.');
return;
}
if (!/^\+\d{8,15}$/.test(customerContact)) {
setFormError('Le contact WhatsApp doit être un numéro de téléphone valide avec le code pays (ex: +2250712345678).');
return;
}
if (!currentUser || !isAdmin) {
showToast('Accès refusé. Vous n\'êtes pas autorisé à modifier les commandes.', 'error');
return;
}
const orderData = {
productName,
customerName,
customerContact,
orderDate: new Date(orderDate).toISOString(),
status,
createdAt: orderToEdit ? orderToEdit.createdAt : new Date().toISOString(),
updatedAt: new Date().toISOString(),
userId: orderToEdit?.userId || 'admin_manual_entry',
};
try {
if (orderToEdit) {
const orderRef = doc(db, `artifacts/${appId}/public/data/all_orders`, orderToEdit.id);
await updateDoc(orderRef, orderData);
if (orderToEdit.userId && orderToEdit.id) {
const userOrderRef = doc(db, `artifacts/${appId}/users/${orderToEdit.userId}/orders`, orderToEdit.id);
const userOrderDoc = await getDoc(userOrderRef);
if (userOrderDoc.exists()) {
await updateDoc(userOrderRef, orderData);
} else {
console.warn(`Document de commande utilisateur non trouvé pour ID ${orderToEdit.id} sous userId ${orderToEdit.userId}.`);
}
}
showToast('Commande mise à jour avec succès !', 'success');
} else {
const docRef = await addDoc(collection(db, `artifacts/${appId}/public/data/all_orders`), orderData);
showToast('Commande ajoutée avec succès !', 'success');
}
onSave();
} catch (error) {
console.error('Erreur lors de la sauvegarde de la commande:', error);
showToast('Erreur lors de la sauvegarde de la commande. Veuillez réessayer.', 'error');
}
};
return (
);
});
// Composant de la modal de connexion administrateur
const AdminAuthModal = React.memo(({ onAuth, onClose, loading }) => {
// Gère l'état de l'email et du mot de passe localement dans la modal
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const modalRef = useRef(null);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
if (modalRef.current) {
modalRef.current.focus();
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
const handleSubmit = () => {
onAuth(email, password); // Passe les valeurs locales à la fonction d'authentification du parent
};
return (
);
});
// Composant pour l'historique des commandes de l'utilisateur
const UserOrdersHistory = React.memo(({ orders }) => {
return (
Mon Historique de Commandes
ID Commande |
Produits |
Total |
Statut |
Date |
{orders.length === 0 ? (
Vous n'avez pas encore passé de commande.
|
) : (
orders.map((order) => (
{order.orderId || order.id}
|
{order.items && order.items.map((item, idx) => (
- {item.name} (x{item.quantity})
))}
|
{order.total} FCFA
|
{order.status}
|
{new Date(order.orderDate).toLocaleDateString('fr-FR')}
|
))
)}
);
});
// Composant AdminPanel (ajouté comme placeholder)
const AdminPanel = React.memo(({ onLogout, currentUser, orders, reviews, showToast, products }) => {
const [selectedProductForEdit, setSelectedProductForEdit] = useState(null);
const [showProductForm, setShowProductForm] = useState(false);
const handleAddProductClick = () => {
setSelectedProductForEdit(null);
setShowProductForm(true);
};
const handleEditProductClick = (product) => {
setSelectedProductForEdit(product);
setShowProductForm(true);
};
const handleDeleteProduct = useCallback(async (productId) => {
try {
await deleteDoc(doc(db, `artifacts/${appId}/public/data/products`, productId));
showToast('Produit supprimé avec succès !', 'success');
} catch (error) {
console.error('Erreur lors de la suppression du produit:', error);
showToast('Erreur lors de la suppression du produit. Veuillez réessayer.', 'error');
}
}, [showToast]);
const handleApproveReview = useCallback(async (reviewId, currentApprovedStatus) => {
try {
const reviewRef = doc(db, `artifacts/${appId}/public/data/reviews`, reviewId);
await updateDoc(reviewRef, { approved: !currentApprovedStatus });
showToast(`Avis ${!currentApprovedStatus ? 'approuvé' : 'désapprouvé'} avec succès !`, 'success');
} catch (error) {
console.error('Erreur lors de la mise à jour de l\'avis:', error);
showToast('Erreur lors de la mise à jour de l\'avis. Veuillez réessayer.', 'error');
}
}, [showToast]);
const handleDeleteReview = useCallback(async (reviewId) => {
try {
await deleteDoc(doc(db, `artifacts/${appId}/public/data/reviews`, reviewId));
showToast('Avis supprimé avec succès !', 'success');
} catch (error) {
console.error('Erreur lors de la suppression de l\'avis:', error);
showToast('Erreur lors de la suppression de l\'avis. Veuillez réessayer.', 'error');
}
}, [showToast]);
// État pour la pagination des produits
const [productsPerPage] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const indexOfLastProduct = currentPage * productsPerPage;
const indexOfFirstProduct = indexOfLastProduct - productsPerPage;
const currentProducts = products.slice(indexOfFirstProduct, indexOfLastProduct);
const totalPages = Math.ceil(products.length / productsPerPage);
const paginate = (pageNumber) => setCurrentPage(pageNumber);
return (
Panneau d'Administration
Bienvenue, Admin {currentUser?.email}. Votre ID: {currentUser?.uid}
{activeAdminTab === 'products' && (
Gestion des Produits ({products.length})
Image |
Nom |
Prix |
Stock |
Promo |
Actions |
{currentProducts.length === 0 ? (
Aucun produit à afficher.
|
) : (
currentProducts.map((product) => (
|
{product.name} |
{product.price} FCFA |
{product.inStock ? 'Oui' : 'Non'}
|
{product.isPromotion ? 'Oui' : 'Non'}
|
|
))
)}
{/* Pagination Controls */}
{Array.from({ length: totalPages }, (_, i) => (
))}
)}
{activeAdminTab === 'orders' && (
Gestion des Commandes ({orders.length})
ID Commande |
Client |
Contact |
Produits |
Total |
Statut |
Date |
Actions |
{orders.length === 0 ? (
Aucune commande à afficher.
|
) : (
orders.map((order) => (
{order.orderId || order.id} |
{order.customerName} |
{order.customerContact} |
{order.items && order.items.map((item, idx) => (
- {item.name} (x{item.quantity})
))}
|
{order.total} FCFA |
{order.status}
|
{new Date(order.orderDate).toLocaleDateString('fr-FR')} |
{/* Ajoutez ici les boutons pour modifier/supprimer la commande si nécessaire */}
|
))
)}
)}
{activeAdminTab === 'reviews' && (
Gestion des Avis ({reviews.length})
Produit ID |
Utilisateur |
Note |
Avis |
Approuvé |
Actions |
{reviews.length === 0 ? (
Aucun avis à afficher.
|
) : (
reviews.map((review) => (
{review.productId} |
{review.userName} |
{review.rating} ⭐ |
{review.text} |
{review.approved ? 'Oui' : 'Non'}
|
|
))
)}
)}
{showProductForm && (
{ setShowProductForm(false); setSelectedProductForEdit(null); }}
onCancel={() => { setShowProductForm(false); setSelectedProductForEdit(null); }}
showToast={showToast}
/>
)}
);
});
// Affichage d'un message de chargement pendant l'initialisation de l'application
if (loading) {
return (
Chargement de l'application...
);
}
// Rendu de l'application principale
return (
{/* Styles CSS pour les animations */}
{/* En-tête de l'application */}
MAMADOU ÉLECTRONIQUE
{/* Affichage des messages d'information/erreur */}
{toast && (
setToast(null)}
/>
)}
{/* Modal de connexion administrateur */}
{adminAuthModal && (
)}
{/* Affichage du panneau administrateur si connecté, sinon l'interface client */}
{isAdmin ? (
) : (
activeAdminTab === 'userOrders' ? (
) : (
Découvrez nos produits électroniques !
Frigos, téléviseurs, machines à laver et bien plus encore, au meilleur prix.
{currentUser && (
Bienvenue ! Votre ID de session (pour le support technique): {currentUser.uid}
)}
{/* Barre de recherche et filtres */}
{/* Section des produits en promotion (filtrés par le terme de recherche) */}
{filteredAndSortedProducts.filter(p => p.isPromotion).length > 0 && (
Nos Promotions !
{filteredAndSortedProducts.filter(p => p.isPromotion).map((product) => (
))}
)}
{/* Section de tous les articles (filtrés et triés) */}
Tous nos Articles
{filteredAndSortedProducts.length === 0 ? (
Aucun produit trouvé avec vos critères.
) : (
{filteredAndSortedProducts.map((product) => (
))}
)}
{/* Affichage des détails du produit sélectionné */}
{selectedProduct && (
setSelectedProduct(null)} onAddToCart={addToCart} currentUser={currentUser} showToast={showToast} />
)}
)
)}
{/* Pied de page de l'application */}
{/* Modal de confirmation globale */}
{showConfirmation && (
{
if (confirmationAction) confirmationAction();
setShowConfirmation(false);
}}
onCancel={() => setShowConfirmation(false)}
/>
)}
{/* Modal du Panier */}
{showCartModal && (
setShowCartModal(false)}
currentUser={currentUser}
showToast={showToast}
loading={loading}
/>
)}
);
}
// Composant pour la modal du panier (inclut le checkout simulé)
const CartModal = React.memo(({ cartItems, onUpdateQuantity, onRemoveItem, onClearCart, onPlaceOrder, onClose, currentUser, showToast, loading }) => {
const modalRef = useRef(null);
const total = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
const [customerName, setCustomerName] = useState(''); // Nom client vide par défaut
const [customerContact, setCustomerContact] = useState('');
const [checkoutError, setCheckoutError] = useState('');
const [showCheckoutForm, setShowCheckoutForm] = useState(false);
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
if (modalRef.current) {
modalRef.current.focus();
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
const handleProceedToCheckout = () => {
if (cartItems.length === 0) {
showToast('Votre panier est vide.', 'error');
return;
}
if (!currentUser) {
showToast('Erreur: Impossible de procéder sans ID utilisateur. Veuillez rafraîchir la page.', 'error');
return;
}
setShowCheckoutForm(true);
};
const handleFinalPlaceOrder = async (e) => {
e.preventDefault();
setCheckoutError('');
if (!customerName.trim() || !customerContact.trim()) {
setCheckoutError('Veuillez remplir votre nom et votre contact.');
return;
}
if (!/^\+\d{8,15}$/.test(customerContact)) {
setCheckoutError('Le contact WhatsApp doit être un numéro de téléphone valide avec le code pays (ex: +2250712345678).');
return;
}
await onPlaceOrder(customerName, customerContact);
setShowCheckoutForm(false);
};
return (
Votre Panier
{cartItems.length === 0 ? (
Votre panier est vide.
) : (
<>
{cartItems.map(item => (
-

{ e.target.onerror = null; e.target.src = `https://placehold.co/50x50/E0F2F7/000000?text=Img`; }}
/>
{item.name}
{item.price} FCFA / unité
{item.quantity}
))}
{!showCheckoutForm && (
)}
{showCheckoutForm && (
)}
>
)}
);
});
export default App;