move to gitea

This commit is contained in:
Mamadou Sall
2025-08-24 22:41:31 +02:00
commit 032b1d9452
122 changed files with 28723 additions and 0 deletions

View File

@@ -0,0 +1,385 @@
// src/components/product/ProductInfo.tsx
'use client';
import { useState } from 'react';
import { WooCommerceProduct } from '@/types/woocommerce';
import { useCartActions } from '@/hooks/useCartSync';import { formatPrice, isOnSale, getDiscountPercentage } from '@/lib/woocommerce';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import {
ShoppingBag,
Heart,
Share2,
Truck,
Shield,
RotateCcw,
Star,
Plus,
Minus
} from 'lucide-react';
import { cn } from '@/lib/utils';
interface ProductInfoProps {
product: WooCommerceProduct;
}
interface ColorOption {
name: string;
value: string;
hex: string;
}
interface SizeOption {
name: string;
value: string;
available: boolean;
}
export default function ProductInfo({ product }: ProductInfoProps): JSX.Element {
const { addToCart, isInCart, getItemQuantity } = useCart();
const [selectedColor, setSelectedColor] = useState<string>('');
const [selectedSize, setSelectedSize] = useState<string>('');
const [quantity, setQuantity] = useState(1);
const [isAddingToCart, setIsAddingToCart] = useState(false);
const [isWishlisted, setIsWishlisted] = useState(false);
const discountPercentage = getDiscountPercentage(product);
const productInCart = isInCart(product.id);
const cartQuantity = getItemQuantity(product.id);
// Extraire les couleurs des attributs du produit
const colorAttribute = product.attributes?.find(attr =>
attr.name.toLowerCase().includes('couleur') ||
attr.name.toLowerCase().includes('color')
);
const sizeAttribute = product.attributes?.find(attr =>
attr.name.toLowerCase().includes('taille') ||
attr.name.toLowerCase().includes('size')
);
const colorOptions: ColorOption[] = colorAttribute?.options.map(color => ({
name: color,
value: color.toLowerCase(),
hex: getColorHex(color),
})) || [];
const sizeOptions: SizeOption[] = sizeAttribute?.options.map(size => ({
name: size,
value: size.toLowerCase(),
available: true, // Vous pouvez ajouter la logique de disponibilité ici
})) || [];
const handleAddToCart = async (): Promise<void> => {
if (isAddingToCart) return;
setIsAddingToCart(true);
try {
addToCart({
id: product.id,
name: product.name,
price: product.sale_price || product.regular_price,
image: product.images[0]?.src || '/placeholder-product.jpg',
}, quantity);
// Simulation d'une petite attente pour l'UX
await new Promise(resolve => setTimeout(resolve, 500));
} finally {
setIsAddingToCart(false);
}
};
const increaseQuantity = (): void => {
setQuantity(prev => prev + 1);
};
const decreaseQuantity = (): void => {
setQuantity(prev => Math.max(1, prev - 1));
};
const toggleWishlist = (): void => {
setIsWishlisted(!isWishlisted);
};
const shareProduct = (): void => {
if (navigator.share) {
navigator.share({
title: product.name,
text: product.short_description,
url: window.location.href,
});
} else {
// Fallback: copier l'URL dans le presse-papiers
navigator.clipboard.writeText(window.location.href);
}
};
return (
<div className="space-y-8">
{/* Product Header */}
<div className="space-y-4">
{/* Categories */}
{product.categories.length > 0 && (
<div className="flex flex-wrap gap-2">
{product.categories.map((category) => (
<Badge key={category.id} variant="outline" className="text-xs">
{category.name}
</Badge>
))}
</div>
)}
{/* Title */}
<h1 className="text-3xl md:text-4xl font-light tracking-wide text-black">
{product.name}
</h1>
{/* Ratings */}
{product.rating_count > 0 && (
<div className="flex items-center gap-2">
<div className="flex items-center">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={cn(
"w-4 h-4",
i < Math.floor(parseFloat(product.average_rating))
? "fill-yellow-400 text-yellow-400"
: "text-gray-300"
)}
/>
))}
</div>
<span className="text-sm text-gray-600">
{product.average_rating} ({product.rating_count} avis)
</span>
</div>
)}
{/* Price */}
<div className="flex items-center gap-4">
{isOnSale(product) ? (
<>
<span className="text-3xl font-medium text-black">
{formatPrice(product.sale_price)}
</span>
<span className="text-xl text-gray-500 line-through">
{formatPrice(product.regular_price)}
</span>
{discountPercentage > 0 && (
<Badge className="bg-red-500 text-white">
-{discountPercentage}%
</Badge>
)}
</>
) : (
<span className="text-3xl font-medium text-black">
{formatPrice(product.price)}
</span>
)}
</div>
{/* Short Description */}
{product.short_description && (
<div
className="text-gray-600 leading-relaxed"
dangerouslySetInnerHTML={{ __html: product.short_description }}
/>
)}
</div>
<Separator />
{/* Product Options */}
<div className="space-y-6">
{/* Colors */}
{colorOptions.length > 0 && (
<div className="space-y-3">
<Label className="text-sm font-medium">
Couleur: {selectedColor && <span className="font-normal">{selectedColor}</span>}
</Label>
<div className="flex flex-wrap gap-3">
{colorOptions.map((color) => (
<button
key={color.value}
onClick={() => setSelectedColor(color.name)}
className={cn(
"w-8 h-8 rounded-full border-2 transition-all duration-200 hover:scale-110",
selectedColor === color.name
? "border-black shadow-md scale-110"
: "border-gray-200"
)}
style={{ backgroundColor: color.hex }}
title={color.name}
/>
))}
</div>
</div>
)}
{/* Sizes */}
{sizeOptions.length > 0 && (
<div className="space-y-3">
<Label className="text-sm font-medium">Taille</Label>
<Select value={selectedSize} onValueChange={setSelectedSize}>
<SelectTrigger>
<SelectValue placeholder="Choisir une taille" />
</SelectTrigger>
<SelectContent>
{sizeOptions.map((size) => (
<SelectItem
key={size.value}
value={size.value}
disabled={!size.available}
>
{size.name} {!size.available && '(Rupture de stock)'}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Quantity */}
<div className="space-y-3">
<Label className="text-sm font-medium">Quantité</Label>
<div className="flex items-center gap-4">
<div className="flex items-center border border-gray-300 rounded-lg">
<Button
variant="ghost"
size="sm"
onClick={decreaseQuantity}
disabled={quantity <= 1}
className="px-3 h-10 hover:bg-gray-100"
>
<Minus className="w-4 h-4" />
</Button>
<span className="px-4 py-2 min-w-[3rem] text-center">{quantity}</span>
<Button
variant="ghost"
size="sm"
onClick={increaseQuantity}
className="px-3 h-10 hover:bg-gray-100"
>
<Plus className="w-4 h-4" />
</Button>
</div>
{cartQuantity > 0 && (
<span className="text-sm text-gray-600">
{cartQuantity} déjà dans le panier
</span>
)}
</div>
</div>
</div>
<Separator />
{/* Action Buttons */}
<div className="space-y-4">
<Button
onClick={handleAddToCart}
disabled={isAddingToCart || !product.purchasable || product.stock_status === 'outofstock'}
className="w-full bg-black text-white hover:bg-gray-800 py-4 text-base uppercase tracking-wide transition-all duration-300"
size="lg"
>
{isAddingToCart ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Ajout en cours...
</div>
) : product.stock_status === 'outofstock' ? (
'Rupture de stock'
) : productInCart ? (
<>
<ShoppingBag className="w-5 h-5 mr-2" />
Ajouter encore ({cartQuantity} dans le panier)
</>
) : (
<>
<ShoppingBag className="w-5 h-5 mr-2" />
Ajouter au panier
</>
)}
</Button>
<div className="flex gap-3">
<Button
variant="outline"
onClick={toggleWishlist}
className={cn(
"flex-1 transition-all duration-300",
isWishlisted && "border-red-500 text-red-500 hover:bg-red-50"
)}
>
<Heart className={cn("w-5 h-5 mr-2", isWishlisted && "fill-current")} />
{isWishlisted ? 'Retiré des favoris' : 'Ajouter aux favoris'}
</Button>
<Button variant="outline" onClick={shareProduct}>
<Share2 className="w-5 h-5 mr-2" />
Partager
</Button>
</div>
</div>
<Separator />
{/* Product Features */}
<div className="space-y-4">
<div className="flex items-center gap-3 text-sm">
<Truck className="w-5 h-5 text-gray-600" />
<span>Livraison gratuite à partir de 50.000 MRU</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Shield className="w-5 h-5 text-gray-600" />
<span>Garantie qualité - Retour sous 30 jours</span>
</div>
<div className="flex items-center gap-3 text-sm">
<RotateCcw className="w-5 h-5 text-gray-600" />
<span>Échange gratuit en magasin</span>
</div>
</div>
</div>
);
}
// Helper function pour les couleurs
function getColorHex(colorName: string): string {
const colorMap: { [key: string]: string } = {
'noir': '#000000',
'blanc': '#ffffff',
'rouge': '#dc2626',
'bleu': '#2563eb',
'vert': '#059669',
'jaune': '#d97706',
'violet': '#7c3aed',
'rose': '#ec4899',
'gris': '#6b7280',
'marron': '#92400e',
'beige': '#d6d3d1',
'doré': '#f59e0b',
'argenté': '#9ca3af',
};
return colorMap[colorName.toLowerCase()] || '#9ca3af';
}
function Label({ children, className = '' }: { children: React.ReactNode; className?: string }): JSX.Element {
return (
<label className={cn('block text-sm font-medium text-gray-900', className)}>
{children}
</label>
);
}