Files
projet_gestion_commande/frontend-prep/src/pages/User/Cart.tsx
T
2026-06-13 14:23:23 +02:00

267 lines
12 KiB
TypeScript

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, ShoppingCart, AlertTriangle, Leaf, X } from "lucide-react";
import "./Cart.css";
interface CartItemWithMedia {
id: number;
product_id: number;
name_product: string;
price: number;
quantity: number;
category: string;
image: string;
hasVideo: boolean;
videoUrl?: string;
}
function Cart() {
const navigate = useNavigate();
const { cartItems, removeFromCart, clearCart, loading, refreshCart } = useCart();
const [showClearConfirm, setShowClearConfirm] = useState(false);
const [showVideo, setShowVideo] = useState(false);
const [currentVideoUrl, setCurrentVideoUrl] = useState<string>("");
const [enrichedItems, setEnrichedItems] = useState<CartItemWithMedia[]>([]);
const [loadingMedia, setLoadingMedia] = useState(false);
useEffect(() => {
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
}, [navigate]);
useEffect(() => {
const interval = setInterval(() => {
if (!isUserAuthenticated()) navigate("/login/client", { replace: true });
}, 5000);
return () => clearInterval(interval);
}, [navigate]);
useEffect(() => {
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
refreshCart();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const total = cartItems.reduce((sum, item) => sum + item.price, 0);
const getProductImage = (product: Product): string => {
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);
}
return "https://via.placeholder.com/120x120/1a1a1a/ffffff?text=No+Image";
};
useEffect(() => {
const enrich = async () => {
if (cartItems.length === 0) { setEnrichedItems([]); return; }
const same =
enrichedItems.length === cartItems.length &&
enrichedItems.every((e, i) => e.id === cartItems[i]?.id && e.product_id === cartItems[i]?.product_id);
if (same) return;
setLoadingMedia(true);
try {
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: { type: string; url?: string }) => 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);
} finally {
setLoadingMedia(false);
}
};
enrich();
}, [cartItems]); // eslint-disable-line react-hooks/exhaustive-deps
const handleClearCart = async () => {
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
setShowClearConfirm(false);
await clearCart();
};
const handleRemoveItem = async (itemId: number) => {
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
await removeFromCart(itemId);
};
const handleCheckout = () => {
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
navigate("/user/checkout");
};
if (loading && cartItems.length === 0) {
return (
<>
<Navbar />
<div className="cart-container">
<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">
{cartItems.length === 0 ? (
<div className="empty-cart">
<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>
) : (
<>
{/* 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>
<button className="clear-all-button" onClick={() => setShowClearConfirm(true)} disabled={loading}>
Tout vider
</button>
</div>
{/* 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">
<Leaf size={20} />
</div>
)}
</div>
{/* Infos */}
<div className="cart-row-info">
<p className="cart-row-name">
{item.name_product}
{item.is_reward && (
<span style={{ marginLeft: "6px", fontSize: "0.7rem", fontWeight: 700, color: "#f59e0b", background: "rgba(245,158,11,0.12)", borderRadius: "4px", padding: "1px 6px" }}>
🎁 Récompense
</span>
)}
</p>
<p className="cart-row-qty">{item.quantity}g</p>
<p className="cart-row-price">
{item.is_reward ? (
<span style={{ color: "#10b981", fontWeight: 700 }}>Offert</span>
) : (
`${item.price.toFixed(2)} €`
)}
</p>
</div>
{/* Bouton supprimer */}
<button
className="cart-row-remove"
onClick={() => handleRemoveItem(item.id)}
disabled={loading}
aria-label={`Supprimer ${item.name_product}`}
>
<Trash2 size={18} />
</button>
</div>
))
)}
</div>
{/* 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>
<button className="checkout-button" onClick={handleCheckout} disabled={loading}>
{loading ? "Chargement..." : "Commander"}
</button>
</div>
</>
)}
</div>
{/* Modal vider le panier */}
{showClearConfirm && (
<div className="modal-overlay" onClick={() => setShowClearConfirm(false)}>
<div className="modal-content2" onClick={(e) => e.stopPropagation()}>
<div className="modal-icon-container">
<AlertTriangle size={40} className="modal-icon" />
</div>
<h2 className="modal-title">Vider le panier</h2>
<p className="modal-message">
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)}>
Annuler
</button>
<button className="modal-button modal-confirm" onClick={handleClearCart} disabled={loading}>
<Trash2 size={16} />
Vider
</button>
</div>
</div>
</div>
)}
{/* Modal vidéo */}
{showVideo && currentVideoUrl && (
<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(""); }}>
<X size={18} />
</button>
<video src={currentVideoUrl} controls autoPlay className="video-player">
Votre navigateur ne supporte pas la lecture de vidéos.
</video>
</div>
</div>
)}
</>
);
}
export default Cart;