chore: add ansible backend docker frontend-prep

This commit is contained in:
2026-01-21 13:05:13 +01:00
parent 5a280b6b01
commit 943fe4de7d
14930 changed files with 2341433 additions and 0 deletions
+528
View File
@@ -0,0 +1,528 @@
// ============================================
// 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/CartContext";
import Navbar from "../../components/Navbar";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { isUserAuthenticated, getProductById } from "../../api/api";
import type { Product } from "../../api/api";
import { Trash2, ShoppingBag, AlertTriangle } 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); // ✨ État pour afficher/masquer la vidéo
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();
}, [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 });
}
}, 5000);
return () => clearInterval(authInterval);
}, [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;
}
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";
}
for (let i = 0; i < product.media.length; i++) {
const mediaItem = product.media[i];
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return `http://localhost:8080${mediaItem.url}`;
}
}
return "https://via.placeholder.com/400x400/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
? `http://localhost:8080${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 =
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;
}
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);
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
// ============================================
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;
}
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;
}
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;
}
navigate("/user/checkout");
};
if (loading && cartItems.length === 0) {
return (
<>
<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>
</>
);
}
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
</button>
</div>
) : (
<>
{loadingMedia && enrichedItems.length === 0 ? (
<div className="loading-cart">
<p>Chargement des médias...</p>
</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,
),
);
}}
/>
{/* ✨ Badge vidéo si disponible */}
{item.hasVideo && item.videoUrl && (
<button
className="cart-video-badge"
onClick={(e) => {
e.stopPropagation();
handleVideoToggle(
item.videoUrl!,
);
}}
aria-label="Voir la vidéo du produit"
>
<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>
</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>
</div>
))}
</div>
)}
<div className="cart-summary">
<div className="summary-row">
<span>Nombre d'articles:</span>
<span>{cartItems.length}</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>
</div>
</>
)}
</div>
{/* Modal de confirmation vidage */}
{showClearConfirm && (
<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" />
</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.
</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={18} />
Tout supprimer
</button>
</div>
</div>
</div>
)}
{/* ✨ Modal vidéo (COMME PRODUCTCARD) */}
{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"
>
<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>
</div>
</div>
)}
</>
);
}
export default Cart;