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
+354
View File
@@ -0,0 +1,354 @@
// ============================================
// context/CartContext.tsx - QUANTITÉS EN GRAMMES
// ============================================
// ✅ quantity = grammes choisis (5, 10, 25, etc.)
// ✅ Pas de boutons +/- dans le panier
// ✅ Pour acheter 2× le même produit, l'ajouter 2 fois
import {
createContext,
useContext,
useState,
useEffect,
type ReactNode,
} from "react";
import {
getCart,
addToCart as apiAddToCart,
removeFromCart as apiRemoveFromCart,
clearCart as apiClearCart,
syncUsernameFromJWT,
getAuthenticatedUsername,
} from "../api/api";
import Toast from "../components/Toast";
// ✅ CartItem - quantity = grammes (5, 10, 25, etc.)
export interface CartItem {
id: number;
product_id: number;
name_product: string;
price: number;
quantity: number; // ✨ En GRAMMES (pas "combien de fois")
category: string;
image?: string;
}
interface CartContextType {
cartItems: CartItem[];
addToCart: (item: Omit<CartItem, "id">) => Promise<void>;
removeFromCart: (id: number) => Promise<void>;
clearCart: () => Promise<void>;
cartCount: number;
cartTotal: number;
loading: boolean;
refreshCart: () => Promise<void>;
showToast: (
message: string,
type: "success" | "error" | "warning" | "info",
) => void;
isAuthenticated: boolean;
}
interface ToastMessage {
id: string;
message: string;
type: "success" | "error" | "warning" | "info";
}
const CartContext = createContext<CartContextType | undefined>(undefined);
export function CartProvider({ children }: { children: ReactNode }) {
const [cartItems, setCartItems] = useState<CartItem[]>([]);
const [loading, setLoading] = useState(false);
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const getUsername = (): string | null => {
return getAuthenticatedUsername();
};
const showToast = (
message: string,
type: "success" | "error" | "warning" | "info" = "success",
) => {
const id = Date.now().toString();
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
};
/**
* ✅ Charger le panier depuis l'API
*/
const refreshCart = async () => {
const username = getUsername();
if (!username) {
console.log("️ [CART] Pas d'utilisateur connecté - panier vide");
setCartItems([]);
setIsAuthenticated(false);
return;
}
setIsAuthenticated(true);
setLoading(true);
try {
console.log("📦 [CART] Récupération du panier pour:", username);
const response = await getCart(username);
console.log("📦 [CART] Réponse complète:", response);
if (response.success && response.panier) {
const panierData = response.panier;
if (
!panierData ||
!Array.isArray(panierData) ||
panierData.length === 0
) {
console.log("✅ [CART] Panier vide");
setCartItems([]);
} else {
// ✅ Transformer les items - quantity = grammes
const items: CartItem[] = panierData.map((item) => {
const category = (item.category || "autre")
.toLowerCase()
.trim();
return {
id: item.id,
product_id: item.product_id,
name_product:
item.product_name ||
item.name_product ||
"Produit",
price: item.price,
quantity: item.quantity, // ✨ En grammes (5, 10, 25, etc.)
category: category,
image: item.image, // Image sera gérée par Cart.tsx avec getProductById
};
});
setCartItems(items);
console.log(
"✅ [CART] Panier chargé:",
items.length,
"articles",
);
}
} else {
console.warn("⚠️ [CART] Réponse invalide:", response);
setCartItems([]);
}
} catch (error) {
console.error("❌ [CART] Erreur fetch:", error);
setCartItems([]);
} finally {
setLoading(false);
}
};
/**
* ✅ Au montage: synchroniser JWT et charger panier
*/
useEffect(() => {
console.log("🔄 [CART] CartProvider montage - synchronisation JWT");
const syncedUsername = syncUsernameFromJWT();
if (syncedUsername) {
setIsAuthenticated(true);
refreshCart();
} else {
setIsAuthenticated(false);
setCartItems([]);
}
return () => {
console.log("🔄 [CART] CartProvider unmount");
};
}, []);
/**
* ✅ Ajouter au panier
* quantity = grammes choisis (5, 10, 25, etc.)
*/
const addToCart = async (item: Omit<CartItem, "id">) => {
const username = getUsername();
if (!username) {
showToast(
"Vous devez être connecté pour ajouter un produit.",
"warning",
);
return;
}
// Validation: quantity doit être > 0
if (!item.quantity || item.quantity <= 0) {
console.error("❌ [ADD] Quantité invalide:", item.quantity);
showToast("Quantité invalide", "error");
return;
}
const cleanName = (item.name_product || "Produit")
.replace(/\s*\([^)]*\)\s*/g, "")
.trim();
const category = (item.category || "autre").toLowerCase().trim();
const requestData = {
username,
name_product: cleanName,
category: category,
quantity: Number(item.quantity), // ✨ Grammes (5, 10, 25)
price: Number(item.price) || 0,
};
console.log("📤 [ADD] Envoi au panier:", requestData);
setLoading(true);
try {
const response = await apiAddToCart(requestData);
if (response.success) {
console.log("✅ [ADD] Produit ajouté avec succès");
await refreshCart();
showToast(
`${cleanName} (${item.quantity}g) ajouté au panier !`,
"success",
);
} else {
console.error("❌ [ADD] Erreur API:", response.message);
showToast(
response.message || "Erreur lors de l'ajout",
"error",
);
}
} catch (error) {
console.error("❌ [ADD] Erreur:", error);
showToast("Erreur lors de l'ajout", "error");
} finally {
setLoading(false);
}
};
/**
* ✅ Supprimer du panier
*/
const removeFromCart = async (id: number) => {
const username = getUsername();
if (!username) {
showToast("Vous devez être connecté.", "warning");
return;
}
setLoading(true);
try {
const response = await apiRemoveFromCart(id, username);
if (response.success) {
await refreshCart();
showToast("Produit supprimé du panier", "success");
} else {
showToast(
response.message || "Erreur lors de la suppression",
"error",
);
}
} catch (error) {
console.error("❌ [REMOVE] Erreur:", error);
showToast("Erreur lors de la suppression", "error");
} finally {
setLoading(false);
}
};
/**
* ✅ Vider le panier
*/
const clearCart = async () => {
const username = getUsername();
if (!username) {
showToast("Vous devez être connecté.", "warning");
return;
}
setLoading(true);
try {
console.log(
"🗑️ [CLEAR] Demande de vidage du panier pour:",
username,
);
const response = await apiClearCart(username);
if (response.success) {
setCartItems([]);
console.log(
"✅ [CLEAR] Panier vidé:",
response.stock_released,
"articles",
);
showToast(response.message || "Panier vidé", "success");
} else {
console.error("❌ [CLEAR] Erreur:", response.message);
showToast(response.message || "Erreur lors du vidage", "error");
}
} catch (error) {
console.error("❌ [CLEAR] Erreur:", error);
showToast("Erreur lors du vidage", "error");
} finally {
setLoading(false);
}
};
// ✅ cartCount = nombre d'articles (pas total des grammes)
const cartCount = cartItems.length;
// ✅ cartTotal = somme des prix (quantity n'est pas utilisé pour le calcul)
const cartTotal = cartItems.reduce((sum, item) => sum + item.price, 0);
const value: CartContextType = {
cartItems,
addToCart,
removeFromCart,
clearCart,
cartCount,
cartTotal,
loading,
refreshCart,
showToast,
isAuthenticated,
};
return (
<CartContext.Provider value={value}>
{children}
{/* Toast notifications */}
<div className="toast-container">
{toasts.map((toast) => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
duration={3000}
/>
))}
</div>
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error("useCart must be used within a CartProvider");
}
return context;
}