268 lines
12 KiB
TypeScript
268 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;
|
|
is_reward?: boolean;
|
|
}
|
|
|
|
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;
|