chore: fix ui

This commit is contained in:
2026-03-31 19:50:44 +02:00
parent ff75c9811a
commit 8fc1335588
7 changed files with 860 additions and 1745 deletions
+125 -389
View File
@@ -1,19 +1,10 @@
// ============================================
// pages/Cart/Cart.tsx - MÊME LOGIQUE QUE PRODUCTCARD
// ============================================
// ✅ Affiche quantity en grammes (5g, 10g, etc.)
// ✅ Affiche photos et vidéos SANS appels API supplémentaires
// ❌ Pas de boutons +/- (on ne modifie pas les grammes)
// ✅ Pour acheter 2× le même produit, l'ajouter 2 fois
// ✅ Vérification continue de l'authentification
import { useCart } from "../../context/useCart";
import Navbar from "../../components/Navbar";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { isUserAuthenticated, getProductById, getMediaUrl } from "../../api/api";
import type { Product } from "../../api/api";
import { Trash2, ShoppingBag, AlertTriangle } from "lucide-react";
import { Trash2, ShoppingCart, AlertTriangle } from "lucide-react";
import "./Cart.css";
interface CartItemWithMedia {
@@ -30,258 +21,89 @@ interface CartItemWithMedia {
function Cart() {
const navigate = useNavigate();
const { cartItems, removeFromCart, clearCart, loading, refreshCart } =
useCart();
const { cartItems, removeFromCart, clearCart, loading, refreshCart } = useCart();
const [showClearConfirm, setShowClearConfirm] = useState(false);
const [showVideo, setShowVideo] = useState(false); // ✨ État pour afficher/masquer la vidéo
const [showVideo, setShowVideo] = useState(false);
const [currentVideoUrl, setCurrentVideoUrl] = useState<string>("");
const [enrichedItems, setEnrichedItems] = useState<CartItemWithMedia[]>([]);
const [loadingMedia, setLoadingMedia] = useState(false);
// ============================================
// HANDLERS VIDÉO (COMME PRODUCTCARD)
// ============================================
const handleVideoToggle = (videoUrl: string) => {
setCurrentVideoUrl(videoUrl);
setShowVideo(true);
};
const handleCloseVideo = () => {
setShowVideo(false);
setCurrentVideoUrl("");
};
// ============================================
// VÉRIFICATION AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isUserAuthenticated()) {
console.log(
"❌ [Cart] Utilisateur non authentifié, redirection vers /login/client",
);
navigate("/login/client", { replace: true });
}
};
checkAuth();
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
}, [navigate]);
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
useEffect(() => {
const authInterval = setInterval(() => {
if (!isUserAuthenticated()) {
console.log(
"❌ [Cart] Session expirée, redirection vers /login/client",
);
navigate("/login/client", { replace: true });
}
const interval = setInterval(() => {
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
}, 5000);
return () => clearInterval(authInterval);
return () => clearInterval(interval);
}, [navigate]);
useEffect(() => {
// ✅ Vérifier l'auth avant de rafraîchir le panier
if (!isUserAuthenticated()) {
console.log("❌ [refreshCart] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
refreshCart();
}, []);
// ============================================
// CALCULS
// ============================================
const total = cartItems.reduce((sum, item) => sum + item.price, 0);
// ============================================
// FONCTIONS POUR PHOTOS ET VIDÉOS (COMME PRODUCTCARD)
// ============================================
const getProductImage = (product: Product): string => {
if (!product.media || product.media.length === 0) {
return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image";
if (!product.media || product.media.length === 0)
return "https://via.placeholder.com/120x120/1a1a1a/ffffff?text=No+Image";
for (const m of product.media) {
if (m && m.type === "image" && m.url) return getMediaUrl(m.url);
}
for (let i = 0; i < product.media.length; i++) {
const mediaItem = product.media[i];
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return getMediaUrl(mediaItem.url);
}
}
return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image";
return "https://via.placeholder.com/120x120/1a1a1a/ffffff?text=No+Image";
};
const hasProductVideo = (product: Product): boolean => {
if (!product.media || product.media.length === 0) {
return false;
}
return product.media.some(
(mediaItem) => mediaItem && mediaItem.type === "video",
);
};
const getProductVideoUrl = (product: Product): string | undefined => {
if (!product.media || product.media.length === 0) {
return undefined;
}
const videoMedia = product.media.find(
(mediaItem) => mediaItem && mediaItem.type === "video",
);
return videoMedia?.url ? getMediaUrl(videoMedia.url) : undefined;
};
// ============================================
// ENRICHIR LES ITEMS AVEC MÉDIAS
// ============================================
useEffect(() => {
const enrichCartItems = async () => {
if (cartItems.length === 0) {
console.log("🔄 [CART] Panier vide, pas d'enrichissement");
setEnrichedItems([]);
setLoadingMedia(false);
return;
}
// ✅ Guard: ne pas re-enrichir si déjà fait pour ces mêmes items
const sameItems =
const enrich = async () => {
if (cartItems.length === 0) { setEnrichedItems([]); return; }
const same =
enrichedItems.length === cartItems.length &&
enrichedItems.every(
(enriched, index) =>
enriched.id === cartItems[index]?.id &&
enriched.product_id === cartItems[index]?.product_id,
);
if (sameItems) {
console.log("🔄 [CART] Items déjà enrichis, skip");
return;
}
enrichedItems.every((e, i) => e.id === cartItems[i]?.id && e.product_id === cartItems[i]?.product_id);
if (same) return;
setLoadingMedia(true);
console.log(
"🔄 [CART] Enrichissement de",
cartItems.length,
"items...",
);
try {
const enrichedPromises = cartItems.map(async (item) => {
try {
console.log(
`📦 [CART] Récupération médias pour produit ${item.product_id}...`,
);
const productResponse = await getProductById(
item.product_id,
);
if (productResponse.success && productResponse.data) {
const product = productResponse.data;
const enriched = {
...item,
image: getProductImage(product),
hasVideo: hasProductVideo(product),
videoUrl: getProductVideoUrl(product),
};
console.log(`✅ [CART] Item enrichi:`, {
name: item.name_product,
image: enriched.image,
hasVideo: enriched.hasVideo,
});
return enriched;
} else {
console.warn(
`⚠️ [CART] Produit ${item.product_id} non trouvé`,
);
return {
...item,
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
hasVideo: false,
videoUrl: undefined,
};
}
} catch (error) {
console.error(
`❌ [CART] Erreur pour produit ${item.product_id}:`,
error,
);
return {
...item,
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
hasVideo: false,
videoUrl: undefined,
};
}
});
const enriched = await Promise.all(enrichedPromises);
const enriched = await Promise.all(
cartItems.map(async (item) => {
try {
const res = await getProductById(item.product_id);
if (res.success && res.data) {
const p = res.data;
const videoMedia = p.media?.find((m: any) => m && m.type === "video");
return {
...item,
image: getProductImage(p),
hasVideo: !!videoMedia,
videoUrl: videoMedia?.url ? getMediaUrl(videoMedia.url) : undefined,
};
}
} catch { /* ignore */ }
return { ...item, image: "https://via.placeholder.com/120x120/1a1a1a/ffffff?text=No+Image", hasVideo: false };
})
);
setEnrichedItems(enriched);
console.log(
"✅ [CART] Enrichissement terminé:",
enriched.length,
"items",
);
} catch (error) {
console.error("❌ [CART] Erreur enrichissement:", error);
setEnrichedItems(
cartItems.map((item) => ({
...item,
image: "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image",
hasVideo: false,
videoUrl: undefined,
})),
);
} finally {
setLoadingMedia(false);
}
};
enrichCartItems();
}, [cartItems]); // ✅ Dépendance sur cartItems complet
// ============================================
// HANDLERS
// ============================================
enrich();
}, [cartItems]);
const handleClearCart = async () => {
// ✅ Vérifier l'auth avant de vider le panier
if (!isUserAuthenticated()) {
console.log("❌ [handleClearCart] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
setShowClearConfirm(false);
await clearCart();
};
const handleRemoveItem = async (itemId: number) => {
// ✅ Vérifier l'auth avant de supprimer un item
if (!isUserAuthenticated()) {
console.log("❌ [handleRemoveItem] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
await removeFromCart(itemId);
};
const handleCheckout = () => {
// ✅ Vérifier l'auth avant de passer commande
if (!isUserAuthenticated()) {
console.log("❌ [handleCheckout] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
navigate("/user/checkout");
};
@@ -290,231 +112,145 @@ function Cart() {
<>
<Navbar />
<div className="cart-container">
<div className="cart-header">
<h1>Votre Panier</h1>
</div>
<div className="loading-cart">
<p>Chargement de votre panier...</p>
</div>
<div className="loading-cart"><p>Chargement du panier...</p></div>
</div>
</>
);
}
const displayItems = enrichedItems.length > 0 ? enrichedItems : cartItems.map(i => ({ ...i, image: "", hasVideo: false }));
return (
<>
<Navbar />
<div className="cart-container">
<div className="cart-header">
<h1>Votre Panier</h1>
{cartItems.length > 0 && (
<button
className="clear-all-button"
onClick={() => setShowClearConfirm(true)}
disabled={loading}
>
<Trash2 size={16} className="trash-icon" />
Tout supprimer
</button>
)}
</div>
{cartItems.length === 0 ? (
<div className="empty-cart">
<ShoppingBag size={48} className="empty-cart-icon" />
<p>Votre panier est vide</p>
<button
className="continue-shopping"
onClick={() => navigate("/user/nos-produits")}
>
Continuer mes achats
<ShoppingCart size={72} className="empty-cart-icon" />
<h2>Panier vide</h2>
<p>Ajoutez des produits pour commencer</p>
<button className="continue-shopping" onClick={() => navigate("/user/nos-produits")}>
Découvrir nos produits
</button>
</div>
) : (
<>
{loadingMedia && enrichedItems.length === 0 ? (
<div className="loading-cart">
<p>Chargement des médias...</p>
{/* Header */}
<div className="cart-header">
<div className="cart-header-left">
<h1 className="cart-title">Mon panier</h1>
<span className="cart-header-count">
{cartItems.length} article{cartItems.length > 1 ? "s" : ""}
</span>
</div>
) : (
<div className="cart-items">
{enrichedItems.map((item, index) => (
<div
key={`${item.id}-${index}`}
className="cart-item"
>
{/* ✨ MEDIA CONTAINER - Photo + Badge Vidéo */}
<div className="cart-item-media-container">
<img
src={item.image}
alt={item.name_product}
className="cart-item-image"
loading="lazy"
onError={(e) => {
console.warn(
`❌ Erreur chargement image pour ${item.name_product}:`,
item.image,
);
e.currentTarget.src =
"https://via.placeholder.com/400x400/7c3aed/ffffff?text=" +
encodeURIComponent(
item.name_product.substring(
0,
10,
),
);
}}
/>
<button className="clear-all-button" onClick={() => setShowClearConfirm(true)} disabled={loading}>
Tout vider
</button>
</div>
{/* ✨ Badge vidéo si disponible */}
{item.hasVideo && item.videoUrl && (
{/* Liste des articles */}
<div className="cart-items-list">
{loadingMedia && enrichedItems.length === 0 ? (
<div className="loading-cart"><p>Chargement des médias...</p></div>
) : (
displayItems.map((item, index) => (
<div key={`${item.id}-${index}`} className="cart-row">
{/* Vignette image */}
<div className="cart-row-thumb">
{item.image ? (
<img
src={item.image}
alt={item.name_product}
className="cart-row-img"
loading="lazy"
onError={(e) => { e.currentTarget.src = "https://via.placeholder.com/72x72/2d2d2d/ffffff?text=?"; }}
/>
) : (
<div className="cart-row-img-placeholder">
<i className="fas fa-leaf"></i>
</div>
)}
{(item as CartItemWithMedia).hasVideo && (item as CartItemWithMedia).videoUrl && (
<button
className="cart-video-badge"
onClick={(e) => {
e.stopPropagation();
handleVideoToggle(
item.videoUrl!,
);
}}
aria-label="Voir la vidéo du produit"
className="cart-row-video-badge"
onClick={(e) => { e.stopPropagation(); setCurrentVideoUrl((item as CartItemWithMedia).videoUrl!); setShowVideo(true); }}
aria-label="Voir la vidéo"
>
<i className="fas fa-camera"></i>
<span>Vidéo</span>
</button>
)}
</div>
<div className="cart-item-info">
<h3>{item.name_product}</h3>
<p className="cart-item-category">
Catégorie: {item.category}
</p>
{/* Quantité en grammes */}
<p className="cart-item-quantity">
Quantité:{" "}
<strong>
{item.quantity}g
</strong>
</p>
<p className="cart-item-price">
{item.price.toFixed(2)}
</p>
{/* Infos */}
<div className="cart-row-info">
<p className="cart-row-name">{item.name_product}</p>
<p className="cart-row-qty">{item.quantity}g</p>
<p className="cart-row-price">{item.price.toFixed(2)} </p>
</div>
{/* Bouton supprimer */}
<div className="cart-item-controls">
<button
className="remove-item-button"
onClick={() =>
handleRemoveItem(item.id)
}
disabled={loading}
title="Supprimer cet article"
aria-label={`Supprimer ${item.name_product}`}
>
<Trash2
size={18}
className="trash-icon"
/>
Supprimer
</button>
</div>
<button
className="cart-row-remove"
onClick={() => handleRemoveItem(item.id)}
disabled={loading}
aria-label={`Supprimer ${item.name_product}`}
>
<Trash2 size={18} />
</button>
</div>
))}
</div>
)}
))
)}
</div>
<div className="cart-summary">
<div className="summary-row">
<span>Nombre d'articles:</span>
<span>{cartItems.length}</span>
{/* Footer fixe */}
<div className="cart-footer">
<div className="cart-footer-total">
<span className="cart-footer-total-label">Total</span>
<span className="cart-footer-total-value">{total.toFixed(2)} </span>
</div>
<div className="summary-row total-row">
<span>Total:</span>
<span className="total-amount">
{total.toFixed(2)}
</span>
</div>
<button
className="checkout-button"
onClick={handleCheckout}
disabled={loading}
>
{loading
? "Chargement..."
: "Valider la commande"}
<button className="checkout-button" onClick={handleCheckout} disabled={loading}>
{loading ? "Chargement..." : "Commander"}
</button>
</div>
</>
)}
</div>
{/* Modal de confirmation vidage */}
{/* Modal vider le panier */}
{showClearConfirm && (
<div
className="modal-overlay"
onClick={() => setShowClearConfirm(false)}
>
<div
className="modal-content2"
onClick={(e) => e.stopPropagation()}
>
<div className="modal-overlay" onClick={() => setShowClearConfirm(false)}>
<div className="modal-content2" onClick={(e) => e.stopPropagation()}>
<div className="modal-icon-container">
<AlertTriangle size={48} className="modal-icon" />
<AlertTriangle size={40} className="modal-icon" />
</div>
<h2 className="modal-title">Vider le panier</h2>
<p className="modal-message">
Êtes-vous sûr de vouloir supprimer tous les articles
de votre panier ? Cette action est irréversible.
Supprimer les {cartItems.length} article{cartItems.length > 1 ? "s" : ""} du panier ?
</p>
<div className="modal-actions">
<button
className="modal-button modal-cancel"
onClick={() => setShowClearConfirm(false)}
>
<button className="modal-button modal-cancel" onClick={() => setShowClearConfirm(false)}>
Annuler
</button>
<button
className="modal-button modal-confirm"
onClick={handleClearCart}
disabled={loading}
>
<Trash2 size={18} />
Tout supprimer
<button className="modal-button modal-confirm" onClick={handleClearCart} disabled={loading}>
<Trash2 size={16} />
Vider
</button>
</div>
</div>
</div>
)}
{/* Modal vidéo (COMME PRODUCTCARD) */}
{/* Modal vidéo */}
{showVideo && currentVideoUrl && (
<div className="video-modal" onClick={handleCloseVideo}>
<div
className="video-modal-content"
onClick={(e) => e.stopPropagation()}
>
<button
className="video-close-btn"
onClick={handleCloseVideo}
aria-label="Fermer la vidéo"
>
<div className="video-modal" onClick={() => { setShowVideo(false); setCurrentVideoUrl(""); }}>
<div className="video-modal-content" onClick={(e) => e.stopPropagation()}>
<button className="video-close-btn" onClick={() => { setShowVideo(false); setCurrentVideoUrl(""); }}>
<i className="fas fa-times"></i>
</button>
<video
src={currentVideoUrl}
controls
autoPlay
className="video-player"
>
Votre navigateur ne supporte pas la lecture de
vidéos.
<video src={currentVideoUrl} controls autoPlay className="video-player">
Votre navigateur ne supporte pas la lecture de vidéos.
</video>
</div>
</div>