chore: add create user route

This commit is contained in:
2026-01-21 16:26:15 +01:00
parent 88d10e96c2
commit f46a5d9652
16 changed files with 1160 additions and 527 deletions
+254 -226
View File
@@ -1,252 +1,280 @@
import { useParams, useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';
import { getProductById, isUserAuthenticated } from '../../api/api';
import type { Product } from '../../api/api';
import { useCart } from '../../context/CartContext';
import Navbar from '../../components/Navbar';
import './ProductDetail.css';
import { useParams, useNavigate } from "react-router-dom";
import { useState, useEffect } from "react";
import { getProductById, isUserAuthenticated } from "../../api/api";
import type { Product } from "../../api/api";
import { useCart } from "../../context/CartContext";
import Navbar from "../../components/Navbar";
import Toast from "../../components/Toast";
import "./ProductDetail.css";
function ProductDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { addToCart } = useCart();
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { addToCart } = useCart();
// floats
const [selectedGrams, setSelectedGrams] = useState<number | null>(null);
const [selectedPrice, setSelectedPrice] = useState<number>(0.0);
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
useEffect(() => {
const checkAuth = () => {
if (!isUserAuthenticated()) {
console.log('❌ [ProductDetail] Utilisateur non authentifié, redirection vers /login/client');
navigate('/login/client', { replace: true });
}
};
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
checkAuth();
}, [navigate]);
// floats
const [selectedGrams, setSelectedGrams] = useState<number | null>(null);
const [selectedPrice, setSelectedPrice] = useState<number>(0.0);
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
useEffect(() => {
const authInterval = setInterval(() => {
if (!isUserAuthenticated()) {
console.log('❌ [ProductDetail] Session expirée, redirection vers /login/client');
navigate('/login/client', { replace: true });
}
}, 5000);
// ✅ TOAST STATE
const [toast, setToast] = useState<{
show: boolean;
message: string;
type: "success" | "error" | "warning" | "info";
}>({
show: false,
message: "",
type: "success",
});
return () => clearInterval(authInterval);
}, [navigate]);
useEffect(() => {
if (id) loadProduct(Number(id));
}, [id]);
const loadProduct = async (productId: number) => {
// ✅ Vérifier l'auth avant de charger le produit
if (!isUserAuthenticated()) {
console.log('❌ [loadProduct] Non authentifié');
navigate('/login/client', { replace: true });
return;
}
setLoading(true);
setError(null);
try {
const response = await getProductById(productId);
if (response.success && response.data) {
const fixedProduct = {
...response.data,
prices: response.data.prices?.map((p: { quantity: number; price: number }) => ({
quantity: parseFloat(String(p.quantity)),
price: parseFloat(String(p.price)),
})) || []
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
useEffect(() => {
const checkAuth = () => {
if (!isUserAuthenticated()) {
console.log(
"❌ [ProductDetail] Utilisateur non authentifié, redirection vers /login/client",
);
navigate("/login/client", { replace: true });
}
};
setProduct(fixedProduct);
checkAuth();
}, [navigate]);
// initialise le prix par défaut (float)
if (fixedProduct.prices.length > 0) {
setSelectedGrams(fixedProduct.prices[0].quantity);
setSelectedPrice(fixedProduct.prices[0].price);
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
useEffect(() => {
const authInterval = setInterval(() => {
if (!isUserAuthenticated()) {
console.log(
"❌ [ProductDetail] Session expirée, redirection vers /login/client",
);
navigate("/login/client", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
useEffect(() => {
if (id) loadProduct(Number(id));
}, [id]);
const loadProduct = async (productId: number) => {
// ✅ Vérifier l'auth avant de charger le produit
if (!isUserAuthenticated()) {
console.log("❌ [loadProduct] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
} else {
setError(response.message || 'Produit non trouvé');
}
} catch (err: any) {
setError(err.message || 'Erreur lors du chargement du produit');
} finally {
setLoading(false);
}
};
const handleGramsChange = (quantity: number) => {
const floatQty = parseFloat(String(quantity));
setSelectedGrams(floatQty);
setLoading(true);
setError(null);
const priceOption = product?.prices?.find(
p => p.quantity === floatQty
);
try {
const response = await getProductById(productId);
if (priceOption) {
setSelectedPrice(parseFloat(String(priceOption.price)));
}
};
const handleAddToCart = () => {
// ✅ Vérifier l'auth avant d'ajouter au panier
if (!isUserAuthenticated()) {
console.log('❌ [handleAddToCart] Non authentifié');
navigate('/login/client', { replace: true });
return;
if (response.success && response.data) {
const fixedProduct = {
...response.data,
prices:
response.data.prices?.map(
(p: { quantity: number; price: number }) => ({
quantity: parseFloat(String(p.quantity)),
price: parseFloat(String(p.price)),
}),
) || [],
};
setProduct(fixedProduct);
// initialise le prix par défaut (float)
if (fixedProduct.prices.length > 0) {
setSelectedGrams(fixedProduct.prices[0].quantity);
setSelectedPrice(fixedProduct.prices[0].price);
}
} else {
setError(response.message || "Produit non trouvé");
}
} catch (err: any) {
setError(err.message || "Erreur lors du chargement du produit");
} finally {
setLoading(false);
}
};
const handleGramsChange = (quantity: number) => {
const floatQty = parseFloat(String(quantity));
setSelectedGrams(floatQty);
const priceOption = product?.prices?.find(
(p) => p.quantity === floatQty,
);
if (priceOption) {
setSelectedPrice(parseFloat(String(priceOption.price)));
}
};
const handleAddToCart = () => {
// ✅ Vérifier l'auth avant d'ajouter au panier
if (!isUserAuthenticated()) {
console.log("❌ [handleAddToCart] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
if (!product || isOutOfStock || selectedGrams === null) {
// ✅ Toast d'erreur si conditions non remplies
setToast({
show: true,
message: "Veuillez sélectionner une quantité",
type: "error",
});
return;
}
addToCart({
product_id: product.id,
name_product: product.name,
category: product.category,
quantity: selectedGrams,
price: selectedPrice,
});
// ✅ Toast de succès
setToast({
show: true,
message: `${product.name} (${selectedGrams}g) ajouté au panier !`,
type: "success",
});
};
if (loading) {
return (
<>
<Navbar />
<div className="product-detail-container">
<div className="loading-container">
<p>Chargement du produit...</p>
</div>
</div>
</>
);
}
if (!product || isOutOfStock || selectedGrams === null) return;
if (error || !product) {
return (
<>
<Navbar />
<div className="product-detail-container">
<div className="error-message">
<h2>{error || "Produit non trouvé"}</h2>
<button
onClick={() => navigate("/user/accueil")}
className="back-button"
>
Retour aux produits
</button>
</div>
</div>
</>
);
}
const isOutOfStock = product.stock === 0;
const hasValidPrices = product.prices && product.prices.length > 0;
addToCart({
product_id: product.id,
name_product: product.name,
category: product.category,
quantity: selectedGrams,
price: selectedPrice,
});
};
// ✅ FIXED: Gestion correcte du type media (string[] | undefined)
const getProductImage = (product: Product): string => {
if (!product.media || product.media.length === 0) {
return '/default-product.jpg';
}
// ✅ product.media est de type string[] selon l'interface Product
const firstMedia = product.media[0];
// ✅ Vérifier si c'est une string directement ou un objet
if (typeof firstMedia === 'string') {
return firstMedia;
}
// ✅ Si c'est un objet avec une propriété url, l'extraire
if (firstMedia && typeof firstMedia === 'object' && 'url' in firstMedia) {
const mediaUrl = (firstMedia as any).url;
return mediaUrl || '/default-product.jpg';
}
return '/default-product.jpg';
};
if (loading) {
return (
<>
<Navbar />
<div className="product-detail-container">
<div className="loading-container">
<p>Chargement du produit...</p>
</div>
</div>
</>
);
}
if (error || !product) {
return (
<>
<Navbar />
<div className="product-detail-container">
<div className="error-message">
<h2>{error || 'Produit non trouvé'}</h2>
<button onClick={() => navigate('/user/accueil')} className="back-button">
Retour aux produits
</button>
</div>
</div>
</>
);
}
const isOutOfStock = product.stock === 0;
const hasValidPrices = product.prices && product.prices.length > 0;
return (
<>
<Navbar />
<>
<Navbar />
<div className="product-detail-container">
<button onClick={() => navigate(-1)} className="back-button">
Retour
</button>
<div className="product-detail-content">
<div className={`product-image-section ${isOutOfStock ? 'out-of-stock' : ''}`}>
<img
src={getProductImage(product)}
alt={product.name}
className="product-detail-image"
/>
{isOutOfStock && <div className="sold-out-badge">SOLD OUT</div>}
</div>
<div className="product-info-section">
<h1 className="product-detail-name">{product.name}</h1>
{selectedPrice > 0 && (
<p className="product-detail-price">
{selectedPrice.toFixed(2)} {selectedGrams && `pour ${selectedGrams}g`}
</p>
{/* ✅ TOAST NOTIFICATION */}
{toast.show && (
<Toast
message={toast.message}
type={toast.type}
duration={3000}
onClose={() => setToast({ ...toast, show: false })}
/>
)}
<div className="product-description">
<h3>Description</h3>
<p>{product.description || 'Aucune description disponible.'}</p>
</div>
<div className="product-stock-info">
{hasValidPrices && (
<div className="grams-selector">
<label htmlFor="grams">Quantité:</label>
<div className="product-detail-container">
<button onClick={() => navigate(-1)} className="back-button">
Retour
</button>
<select
id="grams"
value={selectedGrams ?? ''}
onChange={(e) => handleGramsChange(parseFloat(e.target.value))}
className="grams-dropdown"
disabled={isOutOfStock}
>
<option value="">Choisir une quantité</option>
<div className="product-detail-content">
<div className="product-info-section">
<h1 className="product-detail-name">{product.name}</h1>
{product.prices && product.prices.map((p) => (
<option key={p.quantity} value={p.quantity}>
{p.quantity}g - {p.price.toFixed(2)}
</option>
))}
</select>
{selectedPrice > 0 && (
<p className="product-detail-price">
{selectedPrice.toFixed(2)} {" "}
{selectedGrams && `pour ${selectedGrams}g`}
</p>
)}
<div className="product-description">
<h3>Description</h3>
<p>
{product.description ||
"Aucune description disponible."}
</p>
</div>
<div className="product-stock-info">
{hasValidPrices && (
<div className="grams-selector">
<label htmlFor="grams">Quantité:</label>
<select
id="grams"
value={selectedGrams ?? ""}
onChange={(e) =>
handleGramsChange(
parseFloat(e.target.value),
)
}
className="grams-dropdown"
disabled={isOutOfStock}
>
<option value="">
Choisir une quantité
</option>
{product.prices &&
product.prices.map((p) => (
<option
key={p.quantity}
value={p.quantity}
>
{p.quantity}g -{" "}
{p.price.toFixed(2)}
</option>
))}
</select>
</div>
)}
</div>
<button
className={`add-to-cart-button ${isOutOfStock || selectedGrams === null ? "disabled" : ""}`}
onClick={handleAddToCart}
disabled={isOutOfStock || selectedGrams === null}
>
{isOutOfStock
? "Rupture de stock"
: "Ajouter au panier"}
</button>
</div>
</div>
)}
</div>
<button
className={`add-to-cart-button ${(isOutOfStock || selectedGrams === null) ? 'disabled' : ''}`}
onClick={handleAddToCart}
disabled={isOutOfStock || selectedGrams === null}
>
{isOutOfStock ? 'Rupture de stock' : 'Ajouter au panier'}
</button>
</div>
</div>
</div>
</>
);
</>
);
}
export default ProductDetail;
export default ProductDetail;