chore: add ansible backend docker frontend-prep
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user