Files
projet_gestion_commande/frontend-prep/src/components/ProductCard.tsx
T
2026-09-13 16:11:22 +02:00

339 lines
13 KiB
TypeScript

import { useState } from "react";
import { createPortal } from "react-dom";
import { useNavigate } from "react-router-dom";
import { useCart } from "../context/useCart";
import { Camera, Info, X } from "lucide-react";
import "./ProductCard.css";
function getTextColor(hex: string): string {
const h = hex.replace("#", "");
const full =
h.length === 3
? h
.split("")
.map((c) => c + c)
.join("")
: h;
const r = parseInt(full.slice(0, 2), 16);
const g = parseInt(full.slice(2, 4), 16);
const b = parseInt(full.slice(4, 6), 16);
return (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
}
interface ProductCardProps {
id: number;
name: string;
price: number;
unit: string;
image: string;
stock: number;
category: string;
prices?: Array<{
quantity: number;
price: number;
active_price?: boolean;
promo_price?: number | null;
promo_percent?: number;
}>;
hasVideo?: boolean;
videoUrl?: string; // ✨ Nouveau prop pour l'URL de la vidéo
categoryColor?: string;
coming_soon?: boolean;
}
function ProductCard({
id,
name,
price,
unit = "g",
image,
stock,
category = "autre",
prices,
hasVideo = false,
videoUrl,
categoryColor,
coming_soon,
}: ProductCardProps) {
const navigate = useNavigate();
const { addToCart } = useCart();
const [showQuantitySelect, setShowQuantitySelect] = useState(false);
const [selectedQuantity, setSelectedQuantity] = useState<number | null>(
null,
);
const [showSuccess, setShowSuccess] = useState(false);
const [showVideo, setShowVideo] = useState(false);
const [stockWarning, setStockWarning] = useState<{ wanted: number; available: number } | null>(null);
const isOutOfStock = stock === 0;
const isComingSoon = coming_soon === true;
const normalizedCategory = (category || "autre").toLowerCase().trim();
const firstPromoPrice =
prices?.[0]?.promo_price != null &&
prices[0].promo_price < prices[0].price
? prices[0].promo_price
: null;
const handleDetailsClick = (e: React.MouseEvent) => {
e.stopPropagation();
navigate(`/user/product/${id}`);
};
// ✨ Handler pour afficher/masquer la vidéo
const handleVideoToggle = (e: React.MouseEvent) => {
e.stopPropagation();
setShowVideo(!showVideo);
};
// ✨ Handler pour fermer la vidéo
const handleCloseVideo = (e: React.MouseEvent) => {
e.stopPropagation();
setShowVideo(false);
};
const handleQuickAddClick = (e: React.MouseEvent) => {
e.stopPropagation();
console.log("🔘 Bouton cliqué", {
id,
name,
category: normalizedCategory,
isOutOfStock,
prices: prices?.length || 0,
showQuantitySelect,
});
if (!isOutOfStock && prices && prices.length > 0) {
setShowQuantitySelect(!showQuantitySelect);
}
};
const handleQuantitySelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedGrams = Number(e.target.value);
setSelectedQuantity(selectedGrams);
if (stock > 0 && selectedGrams > stock) {
setStockWarning({ wanted: selectedGrams, available: stock });
return;
}
const priceOption = prices?.find((p) => p.quantity === selectedGrams);
if (priceOption) {
addToCart({
product_id: id,
name_product: name,
price: priceOption.price,
quantity: selectedGrams,
category: normalizedCategory,
image: image,
});
setShowSuccess(true);
setTimeout(() => {
setShowSuccess(false);
setShowQuantitySelect(false);
setSelectedQuantity(null);
}, 1500);
}
};
const cardColor = categoryColor || "#ffffff";
return (
<div
className={`product-card ${isOutOfStock ? "out-of-stock" : ""}`}
style={{ "--category-color": cardColor } as React.CSSProperties}
>
<div className="product-card-content">
<div className="product-image-container">
<img
src={image}
alt={name}
className="product-image3"
loading="lazy"
/>
{/* ✨ Icône caméra cliquable */}
{hasVideo && !isOutOfStock && videoUrl && (
<button
className="media-indicator"
onClick={handleVideoToggle}
aria-label="Voir la vidéo du produit"
>
<Camera size={18} />
</button>
)}
{/* ✨ Bouton "Détails" */}
<button
className="details-button"
onClick={handleDetailsClick}
aria-label="Voir les détails du produit"
>
<Info size={14} />
<span>Détails</span>
</button>
{isOutOfStock && (
<div className="sold-out-overlay">SOLD OUT</div>
)}
{isComingSoon && (
<div className="coming-soon-overlay">COMMING SOON</div>
)}
</div>
<div className="product-info">
<h3 className="product-name">{name}</h3>
<p className="product-price">
{price > 0 ? (
firstPromoPrice !== null ? (
<>
<span className="product-price-strike">
{price.toFixed(2)}
</span>{" "}
{firstPromoPrice.toFixed(2)}
</>
) : (
`${price.toFixed(2)} €`
)
) : (
"Prix non disponible"
)}
</p>
</div>
</div>
<div
className="quick-add-section"
onClick={(e) => e.stopPropagation()}
>
{showSuccess ? (
<div className="success-message"> Ajouté !</div>
) : (
<>
<button
className={`quick-add-btn ${isOutOfStock || isComingSoon ? "disabled" : ""}`}
onClick={handleQuickAddClick}
disabled={isOutOfStock || isComingSoon}
style={
categoryColor && !isOutOfStock && !isComingSoon
? {
background: categoryColor,
color: getTextColor(categoryColor),
}
: undefined
}
>
{isOutOfStock
? "Rupture de stock"
: isComingSoon
? "BIENTÔT DISPONIBLE"
: "Ajouter rapidement"}
</button>
{showQuantitySelect && prices && prices.length > 0 && (
<select
className="quantity-select"
value={selectedQuantity ?? ""}
onChange={handleQuantitySelect}
onClick={(e) => e.stopPropagation()}
style={
categoryColor
? { borderColor: categoryColor }
: undefined
}
>
<option value="">Choisir une quantité</option>
{prices.map((priceOption) => (
<option
key={priceOption.quantity}
value={priceOption.quantity}
>
{priceOption.promo_price != null &&
priceOption.promo_price <
priceOption.price
? `${priceOption.quantity}${unit} - ${priceOption.promo_price.toFixed(2)} € (au lieu de ${priceOption.price.toFixed(2)} €, -${priceOption.promo_percent}%)`
: `${priceOption.quantity}${unit} - ${priceOption.price.toFixed(2)} €`}
</option>
))}
</select>
)}
</>
)}
</div>
{/* Modal stock insuffisant */}
{stockWarning &&
createPortal(
<div className="video-modal" onClick={() => setStockWarning(null)}>
<div
className="video-modal-content"
style={{ maxWidth: 340, padding: "2rem", textAlign: "center" }}
onClick={(e) => e.stopPropagation()}
>
<button
className="video-close-btn"
onClick={() => setStockWarning(null)}
aria-label="Fermer"
>
<X size={18} />
</button>
<div style={{ fontSize: "2.5rem", marginBottom: "0.75rem" }}>⚠️</div>
<p style={{ fontWeight: 700, fontSize: "1.1rem", marginBottom: "0.5rem" }}>
Stock insuffisant
</p>
<p style={{ color: "#aaa", fontSize: "0.95rem", marginBottom: "1.25rem" }}>
Vous avez sélectionné <strong>{stockWarning.wanted}g</strong> mais il ne reste que{" "}
<strong style={{ color: "#ef4444" }}>{stockWarning.available}g</strong> disponible pour <em>{name}</em>.
</p>
<button
style={{
background: "#ef4444",
color: "#fff",
border: "none",
borderRadius: 8,
padding: "0.6rem 1.5rem",
fontWeight: 700,
cursor: "pointer",
}}
onClick={() => setStockWarning(null)}
>
OK
</button>
</div>
</div>,
document.body,
)}
{/* ✨ Modal vidéo — rendu via Portal pour éviter le clipping du transform:scale sur .product-card */}
{showVideo &&
videoUrl &&
createPortal(
<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"
>
<X size={18} />
</button>
<video
src={videoUrl}
controls
autoPlay
className="video-player"
>
Votre navigateur ne supporte pas la lecture de
vidéos.
</video>
</div>
</div>,
document.body,
)}
</div>
);
}
export default ProductCard;