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,234 @@
// src/app/boutique/page.tsx
import { Suspense } from 'react';
import { Metadata } from 'next';
import { WooCommerceService } from '@/lib/woocommerce';
import ProductGrid from '@/components/product/ProductGrid';
import ProductFilters from '@/components/product/ProductFilters';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Grid3X3, List, SlidersHorizontal } from 'lucide-react';
export const metadata: Metadata = {
title: 'Boutique - Collection complète de Melhfa',
description: 'Découvrez notre collection complète de melhfa mauritaniennes. Voiles traditionnels et modernes, accessoires premium et créations artisanales.',
openGraph: {
title: 'Boutique MELHFA - Collection complète',
description: 'Découvrez notre collection complète de melhfa mauritaniennes.',
images: ['/images/boutique-og.jpg'],
},
};
interface BoutiquePageProps {
searchParams: {
page?: string;
category?: string;
filter?: string;
sort?: string;
search?: string;
};
}
export default async function BoutiquePage({
searchParams
}: BoutiquePageProps): Promise<JSX.Element> {
const page = Number(searchParams.page) || 1;
const category = searchParams.category;
const filter = searchParams.filter;
const sort = searchParams.sort;
const search = searchParams.search;
// Construire les paramètres pour l'API WooCommerce
const apiParams: any = {
page,
per_page: 20,
};
if (category) apiParams.category = category;
if (search) apiParams.search = search;
if (filter === 'sale') apiParams.on_sale = true;
if (filter === 'featured') apiParams.featured = true;
if (filter === 'new') {
apiParams.orderby = 'date';
apiParams.order = 'desc';
}
switch (sort) {
case 'price-asc':
apiParams.orderby = 'price';
apiParams.order = 'asc';
break;
case 'price-desc':
apiParams.orderby = 'price';
apiParams.order = 'desc';
break;
case 'name-asc':
apiParams.orderby = 'title';
apiParams.order = 'asc';
break;
case 'name-desc':
apiParams.orderby = 'title';
apiParams.order = 'desc';
break;
default:
apiParams.orderby = 'date';
apiParams.order = 'desc';
}
// Récupérer les produits et catégories
const [productsResponse, categoriesResponse] = await Promise.all([
WooCommerceService.getProducts(apiParams),
WooCommerceService.getCategories(),
]);
const products = productsResponse.data || [];
const categories = categoriesResponse.data || [];
return (
<div className="min-h-screen bg-white">
{/* Header de la page */}
<div className="bg-gray-50 py-16 mt-16">
<div className="max-w-[1400px] mx-auto px-6">
<div className="text-center space-y-4">
<h1 className="text-4xl md:text-5xl font-light tracking-wide text-black">
{filter === 'sale' && 'Promotions'}
{filter === 'featured' && 'Produits Vedettes'}
{filter === 'new' && 'Nouvelles Arrivées'}
{category && `Catégorie: ${category}`}
{search && `Résultats pour: "${search}"`}
{!filter && !category && !search && 'Boutique'}
</h1>
<p className="text-gray-600 max-w-2xl mx-auto">
{filter === 'sale' && 'Profitez de nos offres exceptionnelles sur une sélection de melhfa premium'}
{filter === 'featured' && 'Découvrez nos créations d\'exception, sélectionnées par nos artisans'}
{filter === 'new' && 'Les dernières créations de nos ateliers mauritaniens'}
{!filter && !category && !search && 'Découvrez notre collection complète de melhfa mauritaniennes, alliant tradition et modernité'}
</p>
</div>
</div>
</div>
<div className="max-w-[1400px] mx-auto px-6 py-12">
<div className="flex flex-col lg:flex-row gap-8">
{/* Sidebar Filters */}
<aside className="lg:w-64 flex-shrink-0">
<div className="sticky top-24">
<Suspense fallback={<FiltersSkeleton />}>
<ProductFilters
categories={categories}
currentCategory={category}
currentFilter={filter}
currentSort={sort}
/>
</Suspense>
</div>
</aside>
{/* Main Content */}
<main className="flex-1">
{/* Toolbar */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8 pb-6 border-b border-gray-200">
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">
{products.length} produit{products.length > 1 ? 's' : ''} trouvé{products.length > 1 ? 's' : ''}
</span>
</div>
<div className="flex items-center gap-4">
{/* View Toggle */}
<Tabs defaultValue="grid" className="hidden sm:block">
<TabsList className="grid w-fit grid-cols-2">
<TabsTrigger value="grid" className="px-3">
<Grid3X3 className="w-4 h-4" />
</TabsTrigger>
<TabsTrigger value="list" className="px-3">
<List className="w-4 h-4" />
</TabsTrigger>
</TabsList>
</Tabs>
{/* Mobile Filters */}
<Button
variant="outline"
size="sm"
className="lg:hidden"
>
<SlidersHorizontal className="w-4 h-4 mr-2" />
Filtres
</Button>
</div>
</div>
{/* Products Grid */}
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid
products={products}
currentPage={page}
hasMore={products.length === 20} // Supposer qu'il y a plus si on a 20 produits
/>
</Suspense>
{/* Pagination */}
{products.length === 20 && (
<div className="flex justify-center mt-12">
<Button
variant="outline"
className="px-8"
asChild
>
<a href={`/boutique?${new URLSearchParams({
...searchParams,
page: (page + 1).toString()
}).toString()}`}>
Charger plus de produits
</a>
</Button>
</div>
)}
</main>
</div>
</div>
</div>
);
}
// Composants de skeleton pour le chargement
function FiltersSkeleton(): JSX.Element {
return (
<div className="space-y-6">
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded w-20" />
<div className="space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-3 bg-gray-200 rounded w-full" />
))}
</div>
</div>
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded w-16" />
<div className="space-y-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-3 bg-gray-200 rounded w-full" />
))}
</div>
</div>
</div>
);
}
function ProductGridSkeleton(): JSX.Element {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{[...Array(12)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="aspect-[3/4] bg-gray-200 rounded-lg mb-4" />
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
<div className="h-8 bg-gray-200 rounded w-full" />
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,529 @@
// src/app/checkout/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useCartActions } from '@/hooks/useCartSync';import { formatPrice } from '@/lib/woocommerce';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import {
CreditCard,
Lock,
Truck,
MapPin,
Phone,
Mail,
User,
ShoppingBag,
AlertCircle,
CheckCircle
} from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
interface CheckoutFormData {
// Informations personnelles
firstName: string;
lastName: string;
email: string;
phone: string;
// Adresse de livraison
address: string;
city: string;
postalCode: string;
country: string;
// Options
notes: string;
createAccount: boolean;
newsletterOptIn: boolean;
// Paiement
paymentMethod: 'card' | 'cash' | 'transfer';
}
export default function CheckoutPage(): JSX.Element {
const router = useRouter();
const { cart, clearCart } = useCart();
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<CheckoutFormData>({
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
city: '',
postalCode: '',
country: 'MR',
notes: '',
createAccount: false,
newsletterOptIn: false,
paymentMethod: 'card',
});
// Redirection si le panier est vide
useEffect(() => {
if (cart.items.length === 0) {
router.push('/panier');
}
}, [cart.items.length, router]);
const subtotal = cart.total;
const shipping = subtotal >= 50000 ? 0 : 5000;
const tax = 0; // Pas de TVA en Mauritanie pour la démo
const total = subtotal + shipping + tax;
const handleInputChange = (field: keyof CheckoutFormData, value: string | boolean): void => {
setFormData(prev => ({ ...prev, [field]: value }));
// Effacer l'erreur si elle existe
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
// Validation des champs obligatoires
if (!formData.firstName.trim()) newErrors.firstName = 'Le prénom est requis';
if (!formData.lastName.trim()) newErrors.lastName = 'Le nom est requis';
if (!formData.email.trim()) newErrors.email = 'L\'email est requis';
if (!formData.phone.trim()) newErrors.phone = 'Le téléphone est requis';
if (!formData.address.trim()) newErrors.address = 'L\'adresse est requise';
if (!formData.city.trim()) newErrors.city = 'La ville est requise';
// Validation de l'email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (formData.email && !emailRegex.test(formData.email)) {
newErrors.email = 'Format d\'email invalide';
}
// Validation du téléphone mauritanien
const phoneRegex = /^(\+222|222)?[0-9]{8}$/;
if (formData.phone && !phoneRegex.test(formData.phone.replace(/\s/g, ''))) {
newErrors.phone = 'Format de téléphone invalide (ex: +222 XX XX XX XX)';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
try {
// Simuler l'appel API
await new Promise(resolve => setTimeout(resolve, 2000));
// Simuler le succès
clearCart();
router.push('/checkout/success');
} catch (error) {
console.error('Erreur lors de la commande:', error);
// Gérer l'erreur
} finally {
setIsLoading(false);
}
};
if (cart.items.length === 0) {
return null; // Le useEffect redirigera
}
return (
<div className="min-h-screen bg-gray-50 pt-20">
<div className="max-w-7xl mx-auto px-6 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-light tracking-wide text-black mb-2">
Finaliser votre commande
</h1>
<p className="text-gray-600">
Complétez vos informations pour finaliser votre achat
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Formulaire */}
<div className="lg:col-span-2 space-y-6">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Informations personnelles */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="w-5 h-5" />
Informations personnelles
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">Prénom *</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
placeholder="Votre prénom"
className={errors.firstName ? 'border-red-500' : ''}
/>
{errors.firstName && (
<p className="text-sm text-red-500">{errors.firstName}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Nom *</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
placeholder="Votre nom"
className={errors.lastName ? 'border-red-500' : ''}
/>
{errors.lastName && (
<p className="text-sm text-red-500">{errors.lastName}</p>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email">Email *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="votre@email.com"
className={errors.email ? 'border-red-500' : ''}
/>
{errors.email && (
<p className="text-sm text-red-500">{errors.email}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="phone">Téléphone *</Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="+222 XX XX XX XX"
className={errors.phone ? 'border-red-500' : ''}
/>
{errors.phone && (
<p className="text-sm text-red-500">{errors.phone}</p>
)}
</div>
</div>
</CardContent>
</Card>
{/* Adresse de livraison */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Truck className="w-5 h-5" />
Adresse de livraison
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="address">Adresse *</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
placeholder="Rue, quartier..."
className={errors.address ? 'border-red-500' : ''}
/>
{errors.address && (
<p className="text-sm text-red-500">{errors.address}</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="city">Ville *</Label>
<Select
value={formData.city}
onValueChange={(value) => handleInputChange('city', value)}
>
<SelectTrigger className={errors.city ? 'border-red-500' : ''}>
<SelectValue placeholder="Choisir une ville" />
</SelectTrigger>
<SelectContent>
<SelectItem value="nouakchott">Nouakchott</SelectItem>
<SelectItem value="nouadhibou">Nouadhibou</SelectItem>
<SelectItem value="rosso">Rosso</SelectItem>
<SelectItem value="kaedi">Kaédi</SelectItem>
<SelectItem value="zouerate">Zouérate</SelectItem>
<SelectItem value="atar">Atar</SelectItem>
</SelectContent>
</Select>
{errors.city && (
<p className="text-sm text-red-500">{errors.city}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="postalCode">Code postal</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
placeholder="Code postal"
/>
</div>
<div className="space-y-2">
<Label htmlFor="country">Pays</Label>
<Select
value={formData.country}
onValueChange={(value) => handleInputChange('country', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="MR">Mauritanie</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Instructions de livraison (optionnel)</Label>
<Textarea
id="notes"
value={formData.notes}
onChange={(e) => handleInputChange('notes', e.target.value)}
placeholder="Instructions spéciales pour la livraison..."
rows={3}
/>
</div>
</CardContent>
</Card>
{/* Méthode de paiement */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CreditCard className="w-5 h-5" />
Méthode de paiement
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex items-center space-x-2 p-4 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="paymentMethod"
value="card"
checked={formData.paymentMethod === 'card'}
onChange={(e) => handleInputChange('paymentMethod', e.target.value as 'card')}
className="w-4 h-4"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4" />
<span className="font-medium">Carte bancaire</span>
</div>
<p className="text-sm text-gray-600">Visa, MasterCard</p>
</div>
</div>
<div className="flex items-center space-x-2 p-4 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="paymentMethod"
value="cash"
checked={formData.paymentMethod === 'cash'}
onChange={(e) => handleInputChange('paymentMethod', e.target.value as 'cash')}
className="w-4 h-4"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span className="font-medium">Paiement à la livraison</span>
</div>
<p className="text-sm text-gray-600">Espèces uniquement</p>
</div>
</div>
<div className="flex items-center space-x-2 p-4 border rounded-lg cursor-pointer hover:bg-gray-50">
<input
type="radio"
name="paymentMethod"
value="transfer"
checked={formData.paymentMethod === 'transfer'}
onChange={(e) => handleInputChange('paymentMethod', e.target.value as 'transfer')}
className="w-4 h-4"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<Phone className="w-4 h-4" />
<span className="font-medium">Virement bancaire</span>
</div>
<p className="text-sm text-gray-600">BIM, BMCI, GBM</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Options */}
<Card>
<CardContent className="pt-6 space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="createAccount"
checked={formData.createAccount}
onCheckedChange={(checked) => handleInputChange('createAccount', checked as boolean)}
/>
<Label htmlFor="createAccount" className="text-sm">
Créer un compte pour suivre mes commandes
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter"
checked={formData.newsletterOptIn}
onCheckedChange={(checked) => handleInputChange('newsletterOptIn', checked as boolean)}
/>
<Label htmlFor="newsletter" className="text-sm">
Recevoir les offres et nouveautés par email
</Label>
</div>
</CardContent>
</Card>
</form>
</div>
{/* Résumé de commande */}
<div className="space-y-6">
{/* Articles */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ShoppingBag className="w-5 h-5" />
Votre commande ({cart.itemCount} articles)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{cart.items.map((item) => (
<div key={item.id} className="flex gap-3">
<div className="relative w-16 h-16 flex-shrink-0">
<Image
src={item.image}
alt={item.name}
fill
className="object-cover rounded-lg"
/>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm truncate">{item.name}</h4>
<p className="text-sm text-gray-600">Qté: {item.quantity}</p>
<p className="font-medium">{formatPrice(item.total)}</p>
</div>
</div>
))}
</CardContent>
</Card>
{/* Résumé des prix */}
<Card>
<CardContent className="pt-6 space-y-3">
<div className="flex justify-between">
<span>Sous-total</span>
<span>{formatPrice(subtotal)}</span>
</div>
<div className="flex justify-between">
<span>Livraison</span>
<span>{shipping === 0 ? 'Gratuite' : formatPrice(shipping)}</span>
</div>
{tax > 0 && (
<div className="flex justify-between">
<span>TVA</span>
<span>{formatPrice(tax)}</span>
</div>
)}
<Separator />
<div className="flex justify-between text-lg font-medium">
<span>Total</span>
<span>{formatPrice(total)}</span>
</div>
</CardContent>
</Card>
{/* Bouton de commande */}
<Button
onClick={handleSubmit}
disabled={isLoading}
size="lg"
className="w-full bg-black text-white hover:bg-gray-800 py-4"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Traitement en cours...
</div>
) : (
<>
<Lock className="w-5 h-5 mr-2" />
Confirmer la commande
</>
)}
</Button>
{/* Sécurité */}
<div className="text-center space-y-2">
<div className="flex items-center justify-center gap-2 text-sm text-gray-600">
<Lock className="w-4 h-4" />
<span>Paiement 100% sécurisé</span>
</div>
<p className="text-xs text-gray-500">
Vos données sont protégées par cryptage SSL
</p>
</div>
{/* Retour au panier */}
<Button variant="outline" className="w-full" asChild>
<Link href="/panier">
Retour au panier
</Link>
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,227 @@
// src/app/checkout/success/page.tsx
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
CheckCircle,
Package,
Truck,
Mail,
Phone,
Download,
Home,
ShoppingBag
} from 'lucide-react';
interface OrderDetails {
orderNumber: string;
date: string;
total: string;
paymentMethod: string;
estimatedDelivery: string;
trackingNumber?: string; // Optionnel
}
export default function CheckoutSuccessPage() {
const [orderDetails, setOrderDetails] = useState<OrderDetails | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simuler la récupération des détails de commande
const timer = setTimeout(() => {
setOrderDetails({
orderNumber: `MELHFA-${Date.now().toString().slice(-6)}`,
date: new Date().toLocaleDateString('fr-FR'),
total: '85.000 MRU',
paymentMethod: 'Carte bancaire',
estimatedDelivery: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toLocaleDateString('fr-FR'),
// trackingNumber sera undefined - pas de problème avec l'interface
});
setIsLoading(false);
}, 1000);
return () => clearTimeout(timer);
}, []);
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 pt-20 flex items-center justify-center">
<div className="text-center space-y-4">
<div className="w-16 h-16 border-4 border-black border-t-transparent rounded-full animate-spin mx-auto" />
<p className="text-gray-600">Traitement de votre commande...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 pt-20">
<div className="max-w-4xl mx-auto px-6 py-16">
{/* Success Header */}
<div className="text-center mb-12">
<div className="w-24 h-24 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-12 h-12 text-green-600" />
</div>
<h1 className="text-4xl font-light tracking-wide text-black mb-4">
Commande confirmée !
</h1>
<p className="text-xl text-gray-600 mb-2">
Merci pour votre achat
</p>
{orderDetails && (
<p className="text-gray-600">
Commande n° <span className="font-medium">{orderDetails.orderNumber}</span>
</p>
)}
</div>
{/* Order Details */}
{orderDetails && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
{/* Order Summary */}
<Card>
<CardContent className="p-6">
<h2 className="text-lg font-medium mb-6 flex items-center gap-2">
<Package className="w-5 h-5" />
Détails de la commande
</h2>
<div className="space-y-4">
<div className="flex justify-between">
<span className="text-gray-600">Numéro de commande</span>
<span className="font-medium">{orderDetails.orderNumber}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Date</span>
<span className="font-medium">{orderDetails.date}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Total</span>
<span className="font-medium text-lg">{orderDetails.total}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Paiement</span>
<Badge variant="secondary">{orderDetails.paymentMethod}</Badge>
</div>
<div className="border-t pt-4">
<div className="flex justify-between">
<span className="text-gray-600">Statut</span>
<Badge className="bg-green-100 text-green-800">
Confirmée
</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Delivery Info */}
<Card>
<CardContent className="p-6">
<h2 className="text-lg font-medium mb-6 flex items-center gap-2">
<Truck className="w-5 h-5" />
Informations de livraison
</h2>
<div className="space-y-4">
<div className="flex justify-between">
<span className="text-gray-600">Livraison estimée</span>
<span className="font-medium">{orderDetails.estimatedDelivery}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Méthode</span>
<span className="font-medium">Livraison standard</span>
</div>
{orderDetails.trackingNumber ? (
<div className="flex justify-between">
<span className="text-gray-600">Suivi</span>
<Button variant="link" className="p-0 h-auto text-blue-600">
{orderDetails.trackingNumber}
</Button>
</div>
) : (
<div className="bg-blue-50 p-4 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Suivi de commande :</strong> Vous recevrez un numéro de suivi par email dès que votre commande sera expédiée.
</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
)}
{/* Next Steps */}
<Card className="mb-8">
<CardContent className="p-6">
<h2 className="text-lg font-medium mb-6">Prochaines étapes</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3">
<Mail className="w-6 h-6 text-blue-600" />
</div>
<h3 className="font-medium mb-2">Confirmation par email</h3>
<p className="text-sm text-gray-600">
Vous allez recevoir un email de confirmation avec tous les détails de votre commande.
</p>
</div>
<div className="text-center">
<div className="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center mx-auto mb-3">
<Package className="w-6 h-6 text-yellow-600" />
</div>
<h3 className="font-medium mb-2">Préparation</h3>
<p className="text-sm text-gray-600">
Votre commande est en cours de préparation dans nos ateliers.
</p>
</div>
<div className="text-center">
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
<Truck className="w-6 h-6 text-green-600" />
</div>
<h3 className="font-medium mb-2">Livraison</h3>
<p className="text-sm text-gray-600">
Livraison estimée le {orderDetails?.estimatedDelivery} à votre adresse.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button size="lg" className="bg-black text-white hover:bg-gray-800" asChild>
<Link href="/boutique">
<ShoppingBag className="w-5 h-5 mr-2" />
Continuer mes achats
</Link>
</Button>
<Button variant="outline" size="lg" asChild>
<Link href="/">
<Home className="w-5 h-5 mr-2" />
Retour à l'accueil
</Link>
</Button>
</div>
</div>
</div>
);
}

BIN
src_backup/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

430
src_backup/app/globals.css Normal file
View File

@@ -0,0 +1,430 @@
/* ============================================== */
/* GLOBALS.CSS FINAL CORRIGÉ - MELHFA E-COMMERCE */
/* Compatible Tailwind v3 + Next.js 14 - SANS ERREURS */
/* ============================================== */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ============================================== */
/* VARIABLES CSS CUSTOM */
/* ============================================== */
@layer base {
:root {
/* Couleurs de base */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Composants UI */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* Couleurs primaires */
--primary: 222.2 84% 4.9%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
/* États et interactions */
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
/* Bordures et inputs */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
/* Border radius global */
--radius: 0.5rem;
}
/* Mode sombre */
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
/* ============================================== */
/* STYLES DE BASE - SANS CLASSES PROBLÉMATIQUES */
/* ============================================== */
@layer base {
html {
scroll-behavior: smooth;
font-family: system-ui, sans-serif;
}
body {
margin: 0;
background: hsl(var(--background));
color: hsl(var(--foreground));
font-feature-settings: "rlig" 1, "calt" 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
}
/* Suppression des bordures par défaut */
*,
*::before,
*::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: hsl(var(--border));
}
/* Scrollbar personnalisée */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Sélection de texte */
::selection {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
}
/* ============================================== */
/* COMPOSANTS PERSONNALISÉS */
/* ============================================== */
@layer components {
/* Effets de hover pour cartes */
.card-hover {
@apply transition-all duration-300 hover:shadow-lg hover:-translate-y-1;
}
/* Effet zoom pour images */
.image-zoom {
@apply transition-transform duration-700 hover:scale-105;
}
/* Container parallax */
.parallax {
transform: translateZ(0);
will-change: transform;
}
/* Grid responsive pour produits */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
@media (max-width: 640px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
}
/* Styles de focus Ring personnalisés */
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2;
}
/* Estados de status/badges */
.status-success {
@apply bg-green-100 text-green-800 border border-green-200 px-3 py-1 rounded-full text-sm font-medium;
}
.status-warning {
@apply bg-yellow-100 text-yellow-800 border border-yellow-200 px-3 py-1 rounded-full text-sm font-medium;
}
.status-error {
@apply bg-red-100 text-red-800 border border-red-200 px-3 py-1 rounded-full text-sm font-medium;
}
.status-info {
@apply bg-blue-100 text-blue-800 border border-blue-200 px-3 py-1 rounded-full text-sm font-medium;
}
/* Cartes de produits MELHFA */
.melhfa-card {
@apply bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden transition-all duration-300 hover:shadow-md hover:-translate-y-1;
}
/* Boutons personnalisés */
.btn-primary {
@apply bg-black text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 hover:bg-gray-800 focus:ring-2 focus:ring-black focus:ring-offset-2;
}
.btn-secondary {
@apply bg-gray-100 text-gray-900 px-6 py-3 rounded-lg font-medium transition-all duration-200 hover:bg-gray-200 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2;
}
}
/* ============================================== */
/* UTILITAIRES PERSONNALISÉS */
/* ============================================== */
@layer utilities {
/* Espacement de sections */
.section-padding {
@apply py-16 md:py-20 lg:py-24;
}
.container-padding {
@apply px-6 lg:px-8;
}
/* Typographie */
.heading-xl {
@apply text-4xl md:text-5xl lg:text-6xl font-light tracking-tight leading-none;
}
.heading-lg {
@apply text-3xl md:text-4xl font-light tracking-wide leading-tight;
}
.heading-md {
@apply text-2xl md:text-3xl font-light tracking-wide;
}
.body-lg {
@apply text-lg md:text-xl text-gray-600 leading-relaxed;
}
.body-sm {
@apply text-sm text-gray-600 leading-relaxed;
}
/* Équilibrage du texte */
.text-balance {
text-wrap: balance;
}
/* Utilitaires de layout */
.center-absolute {
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
}
.center-flex {
@apply flex items-center justify-center;
}
/* Ratios d'aspect personnalisés */
.aspect-product {
aspect-ratio: 3 / 4;
}
.aspect-hero {
aspect-ratio: 16 / 9;
}
/* Safe area pour mobile */
.safe-top {
padding-top: env(safe-area-inset-top);
}
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
/* Masquer scrollbar */
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
/* Conteneur max-width pour MELHFA */
.container-melhfa {
@apply max-w-[1400px] mx-auto px-6;
}
}
/* ============================================== */
/* ANIMATIONS PERSONNALISÉES */
/* ============================================== */
/* Keyframes */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-in {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounce-subtle {
0%,
20%,
53%,
80%,
100% {
transform: translate3d(0, 0, 0);
}
40%,
43% {
transform: translate3d(0, -8px, 0);
}
70% {
transform: translate3d(0, -4px, 0);
}
90% {
transform: translate3d(0, -2px, 0);
}
}
/* Classes d'animation */
.animate-fade-in-up {
animation: fade-in-up 0.8s ease-out;
}
.animate-slide-in {
animation: slide-in 0.5s ease-out;
}
.animate-scale-in {
animation: scale-in 0.3s ease-out;
}
.animate-bounce-subtle {
animation: bounce-subtle 1s ease-in-out;
}
/* ============================================== */
/* STYLES RESPONSIFS */
/* ============================================== */
@media (max-width: 640px) {
.heading-xl {
@apply text-3xl;
}
.heading-lg {
@apply text-2xl;
}
.section-padding {
@apply py-12;
}
}
/* ============================================== */
/* ACCESSIBILITÉ */
/* ============================================== */
@media (prefers-contrast: high) {
.melhfa-card {
@apply border-2 border-gray-800;
}
}
@media (prefers-reduced-motion: reduce) {
.animate-fade-in-up,
.animate-slide-in,
.animate-scale-in,
.animate-bounce-subtle,
.card-hover,
.image-zoom {
animation: none;
transition: none;
}
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* ============================================== */
/* STYLES D'IMPRESSION */
/* ============================================== */
@media print {
.no-print {
display: none !important;
}
body {
background: white !important;
color: black !important;
}
.melhfa-card {
@apply border border-gray-400 shadow-none;
}
}

222
src_backup/app/layout.tsx Normal file
View File

@@ -0,0 +1,222 @@
// src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { cn } from '@/lib/utils';
import Header from '@/components/layout/Header';
import Footer from '@/components/layout/Footer';
import './globals.css';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
export const metadata: Metadata = {
title: {
default: 'MELHFA - Voiles Mauritaniens Premium',
template: '%s | MELHFA'
},
description: 'Découvrez l\'art mauritanien à travers nos voiles d\'exception, alliant tradition ancestrale et élégance contemporaine. Boutique en ligne de melhfa premium.',
keywords: [
'melhfa',
'voile mauritanien',
'melhfa traditionnelle',
'mode mauritanienne',
'artisanat mauritanien',
'melhfa premium',
'boutique en ligne mauritanie',
'nouakchott',
'voile africain'
],
authors: [{ name: 'MELHFA' }],
creator: 'MELHFA',
publisher: 'MELHFA',
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://melhfa.com'),
alternates: {
canonical: '/',
},
openGraph: {
type: 'website',
locale: 'fr_FR',
url: '/',
siteName: 'MELHFA',
title: 'MELHFA - Voiles Mauritaniens Premium',
description: 'Découvrez l\'art mauritanien à travers nos voiles d\'exception, alliant tradition ancestrale et élégance contemporaine.',
images: [
{
url: '/images/og-image.jpg',
width: 1200,
height: 630,
alt: 'MELHFA - Voiles Mauritaniens Premium',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'MELHFA - Voiles Mauritaniens Premium',
description: 'Découvrez l\'art mauritanien à travers nos voiles d\'exception, alliant tradition ancestrale et élégance contemporaine.',
images: ['/images/twitter-image.jpg'],
creator: '@melhfa',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
verification: {
google: process.env.GOOGLE_VERIFICATION_ID,
},
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
return (
<html lang="fr" className={cn(inter.variable, 'scroll-smooth')}>
<head>
{/* Preconnect pour optimiser les performances */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
{/* Favicon */}
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
{/* Theme color for mobile browsers */}
<meta name="theme-color" content="#000000" />
{/* Preload critical resources */}
<link
rel="preload"
href="/images/melhfa-hero.jpg"
as="image"
type="image/jpeg"
/>
</head>
<body
className={cn(
'min-h-screen bg-white font-sans antialiased',
inter.className
)}
suppressHydrationWarning
>
{/* Skip to main content for accessibility */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-black text-white px-4 py-2 rounded-md z-50"
>
Aller au contenu principal
</a>
{/* Header */}
<Header />
{/* Main Content */}
<main id="main-content" className="min-h-screen">
{children}
</main>
{/* Footer */}
<Footer />
{/* Scripts for analytics, etc. */}
{process.env.NODE_ENV === 'production' && (
<>
{/* Google Analytics */}
{process.env.NEXT_PUBLIC_GA_ID && (
<>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${process.env.NEXT_PUBLIC_GA_ID}');
`,
}}
/>
</>
)}
{/* Facebook Pixel */}
{process.env.NEXT_PUBLIC_FB_PIXEL_ID && (
<script
dangerouslySetInnerHTML={{
__html: `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${process.env.NEXT_PUBLIC_FB_PIXEL_ID}');
fbq('track', 'PageView');
`,
}}
/>
)}
</>
)}
{/* Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'ClothingStore',
name: 'MELHFA',
description: 'Boutique en ligne de voiles mauritaniens premium et accessoires traditionnels',
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://melhfa.com',
logo: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://melhfa.com'}/images/logo.png`,
image: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://melhfa.com'}/images/og-image.jpg`,
telephone: '+222 XX XX XX XX',
address: {
'@type': 'PostalAddress',
streetAddress: 'Nouakchott',
addressLocality: 'Nouakchott',
addressCountry: 'MR'
},
geo: {
'@type': 'GeoCoordinates',
latitude: '18.0735',
longitude: '-15.9582'
},
sameAs: [
'https://facebook.com/melhfa',
'https://instagram.com/melhfa',
'https://twitter.com/melhfa'
],
paymentAccepted: ['Carte de crédit', 'Virement bancaire', 'Espèces'],
currenciesAccepted: 'MRU',
areaServed: 'Mauritanie'
})
}}
/>
</body>
</html>
);
}

214
src_backup/app/page.tsx Normal file
View File

@@ -0,0 +1,214 @@
// src/app/page.tsx
import { Suspense } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { WooCommerceService } from '@/lib/woocommerce';
import ProductCard from '@/components/product/ProductCard';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ArrowRight, Star, Truck, Shield, Heart } from 'lucide-react';
// Composants pour les sections de la page d'accueil
import HeroSection from '@/components/sections/HeroSection';
import FeaturedProductsSection from '@/components/sections/FeaturedProductsSection';
import PromoSection from '@/components/sections/PromoSection';
import NewsletterSection from '@/components/sections/NewsletterSection';
export default async function HomePage(): Promise<JSX.Element> {
// Récupérer les données des produits
const [featuredProducts, saleProducts, newArrivals] = await Promise.all([
WooCommerceService.getFeaturedProducts(6),
WooCommerceService.getSaleProducts(4),
WooCommerceService.getNewArrivals(8),
]);
return (
<main className="min-h-screen bg-white">
{/* Hero Section */}
<HeroSection />
{/* Promo Banner */}
<PromoSection />
{/* Featured Products */}
<section className="py-20 bg-gray-50">
<div className="max-w-[1400px] mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl font-light mb-4 tracking-wide">
Produits Vedettes
</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Découvrez notre sélection de melhfa d&apos;exception, alliant tradition mauritanienne et élégance contemporaine
</p>
</div>
{/* Grid de produits vedettes avec design asymétrique */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
{/* Produit principal - grande taille */}
<div className="lg:row-span-2">
<div className="relative h-[600px] lg:h-[824px] overflow-hidden rounded-lg group cursor-pointer">
<Image
src="/images/melhfa-featured-1.jpg"
alt="Collection Sahara"
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-6 right-6">
<Badge className="bg-red-500 text-white">-25%</Badge>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-8 text-white transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
<h3 className="text-2xl font-light mb-3 tracking-wide">Collection Sahara</h3>
<p className="text-sm opacity-90 mb-4">
Melhfa artisanale aux motifs berbères traditionnels, tissée à la main dans nos ateliers de Nouakchott.
</p>
<div className="flex items-center justify-between">
<div className="text-xl font-medium">
42.000 MRU{" "}
<span className="text-sm line-through opacity-70">56.000 MRU</span>
</div>
<Button className="bg-white text-black hover:bg-gray-100">
Découvrir
</Button>
</div>
</div>
</div>
</div>
{/* Produits secondaires */}
<div className="space-y-6">
<div className="relative h-[400px] overflow-hidden rounded-lg group cursor-pointer">
<Image
src="/images/melhfa-featured-2.jpg"
alt="Melhfa Élégance"
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-6 text-white transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
<h3 className="text-xl font-light mb-2 tracking-wide">Melhfa Élégance</h3>
<p className="text-sm opacity-90 mb-3">Design moderne pour femme active</p>
<div className="text-lg font-medium">32.000 MRU</div>
</div>
</div>
<div className="relative h-[400px] overflow-hidden rounded-lg group cursor-pointer">
<Image
src="/images/melhfa-featured-3.jpg"
alt="Melhfa Océan"
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-6 right-6">
<Badge className="bg-red-500 text-white">-15%</Badge>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-6 text-white transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
<h3 className="text-xl font-light mb-2 tracking-wide">Melhfa Océan</h3>
<p className="text-sm opacity-90 mb-3">Nuances bleues inspirées de l&apos;Atlantique</p>
<div className="text-lg font-medium">
29.000 MRU{" "}
<span className="text-sm line-through opacity-70">34.000 MRU</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* New Arrivals Products */}
<section className="py-20">
<div className="max-w-[1400px] mx-auto px-6">
<div className="flex items-center justify-between mb-16">
<div>
<h2 className="text-4xl font-light mb-4 tracking-wide">Nouvelle Collection</h2>
<p className="text-gray-600">Les dernières créations de nos artisans</p>
</div>
<Button variant="outline" className="hidden md:flex" asChild>
<Link href="/boutique">
Voir tout
<ArrowRight className="w-4 h-4 ml-2" />
</Link>
</Button>
</div>
<Suspense fallback={<ProductGridSkeleton />}>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{newArrivals.success && newArrivals.data.map((product) => (
<ProductCard
key={product.id}
product={product}
className="animate-fade-in-up"
/>
))}
</div>
</Suspense>
<div className="text-center mt-12 md:hidden">
<Button variant="outline" asChild>
<Link href="/boutique">
Voir tout
<ArrowRight className="w-4 h-4 ml-2" />
</Link>
</Button>
</div>
</div>
</section>
{/* Features Section */}
<section className="py-20 bg-gray-50">
<div className="max-w-[1400px] mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center group">
<div className="w-16 h-16 mx-auto mb-4 bg-black rounded-full flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Truck className="w-8 h-8 text-white" />
</div>
<h3 className="text-lg font-medium mb-2">Livraison gratuite</h3>
<p className="text-sm text-gray-600">À partir de 50.000 MRU dans tout Nouakchott</p>
</div>
<div className="text-center group">
<div className="w-16 h-16 mx-auto mb-4 bg-black rounded-full flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Shield className="w-8 h-8 text-white" />
</div>
<h3 className="text-lg font-medium mb-2">Garantie qualité</h3>
<p className="text-sm text-gray-600">Retour gratuit sous 30 jours</p>
</div>
<div className="text-center group">
<div className="w-16 h-16 mx-auto mb-4 bg-black rounded-full flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<Heart className="w-8 h-8 text-white" />
</div>
<h3 className="text-lg font-medium mb-2">Fait main</h3>
<p className="text-sm text-gray-600">Chaque melhfa est unique et artisanale</p>
</div>
</div>
</div>
</section>
{/* Newsletter Section */}
<NewsletterSection />
</main>
);
}
// Composant de skeleton pour le chargement
function ProductGridSkeleton(): JSX.Element {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{[...Array(8)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="aspect-[3/4] bg-gray-200 rounded-lg mb-4" />
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,322 @@
// src/app/panier/page.tsx
'use client';
import { useCartActions } from '@/hooks/useCartSync';import { formatPrice } from '@/lib/woocommerce';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import {
ShoppingBag,
Trash2,
Plus,
Minus,
ArrowRight,
Truck,
Shield,
Tag
} from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import { cn } from '@/lib/utils';
export default function CartPage(): JSX.Element {
const { cart, updateQuantity, removeFromCart, clearCart } = useCart();
const [promoCode, setPromoCode] = useState('');
const [isPromoApplied, setIsPromoApplied] = useState(false);
const [promoDiscount, setPromoDiscount] = useState(0);
const subtotal = cart.total;
const shipping = subtotal >= 50000 ? 0 : 5000; // Livraison gratuite à partir de 50.000 MRU
const discount = isPromoApplied ? promoDiscount : 0;
const total = subtotal + shipping - discount;
const handleQuantityChange = (productId: number, newQuantity: number): void => {
if (newQuantity <= 0) {
removeFromCart(productId);
} else {
updateQuantity(productId, newQuantity);
}
};
const applyPromoCode = (): void => {
// Codes promo factices pour la démo
const promoCodes: { [key: string]: number } = {
'WELCOME10': 0.1,
'SUMMER20': 0.2,
'FIRST5': 0.05,
};
const discountPercent = promoCodes[promoCode.toUpperCase()];
if (discountPercent) {
setIsPromoApplied(true);
setPromoDiscount(subtotal * discountPercent);
}
};
const removePromoCode = (): void => {
setIsPromoApplied(false);
setPromoDiscount(0);
setPromoCode('');
};
if (cart.items.length === 0) {
return (
<div className="min-h-screen bg-white pt-20">
<div className="max-w-[1400px] mx-auto px-6 py-16">
<div className="text-center space-y-8">
<div className="w-32 h-32 mx-auto bg-gray-100 rounded-full flex items-center justify-center">
<ShoppingBag className="w-16 h-16 text-gray-400" />
</div>
<div className="space-y-4">
<h1 className="text-3xl font-light tracking-wide">Votre panier est vide</h1>
<p className="text-gray-600 max-w-md mx-auto">
Découvrez notre collection de melhfa exceptionnelles et commencez votre shopping.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button asChild size="lg" className="bg-black text-white hover:bg-gray-800">
<Link href="/boutique">
Découvrir la boutique
<ArrowRight className="w-5 h-5 ml-2" />
</Link>
</Button>
<Button variant="outline" size="lg" asChild>
<Link href="/">Retour à l'accueil</Link>
</Button>
</div>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-white pt-20">
<div className="max-w-[1400px] mx-auto px-6 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl md:text-4xl font-light tracking-wide text-black mb-2">
Panier ({cart.itemCount} article{cart.itemCount > 1 ? 's' : ''})
</h1>
<p className="text-gray-600">Vérifiez vos articles avant de procéder au paiement</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Cart Items */}
<div className="lg:col-span-2 space-y-6">
{/* Clear Cart Button */}
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">
{cart.items.length} produit{cart.items.length > 1 ? 's' : ''} dans votre panier
</span>
<Button
variant="ghost"
size="sm"
onClick={clearCart}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4 mr-2" />
Vider le panier
</Button>
</div>
{/* Cart Items List */}
<div className="space-y-4">
{cart.items.map((item) => (
<Card key={item.id}>
<CardContent className="p-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Product Image */}
<div className="relative w-full md:w-32 h-48 md:h-32 flex-shrink-0">
<Image
src={item.image}
alt={item.name}
fill
className="object-cover rounded-lg"
sizes="(max-width: 768px) 100vw, 128px"
/>
</div>
{/* Product Info */}
<div className="flex-1 space-y-3">
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium text-lg">{item.name}</h3>
<p className="text-gray-600 text-sm">Prix unitaire: {formatPrice(item.price)}</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeFromCart(item.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 p-2"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
<div className="flex justify-between items-center">
{/* Quantity Controls */}
<div className="flex items-center border border-gray-300 rounded-lg">
<Button
variant="ghost"
size="sm"
onClick={() => handleQuantityChange(item.id, item.quantity - 1)}
className="px-3 h-10 hover:bg-gray-100"
disabled={item.quantity <= 1}
>
<Minus className="w-4 h-4" />
</Button>
<span className="px-4 py-2 min-w-[3rem] text-center font-medium">
{item.quantity}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleQuantityChange(item.id, item.quantity + 1)}
className="px-3 h-10 hover:bg-gray-100"
>
<Plus className="w-4 h-4" />
</Button>
</div>
{/* Item Total */}
<div className="text-right">
<p className="font-medium text-lg">{formatPrice(item.total)}</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
{/* Cart Summary */}
<div className="space-y-6">
{/* Promo Code */}
<Card>
<CardContent className="p-6 space-y-4">
<h3 className="font-medium flex items-center gap-2">
<Tag className="w-5 h-5" />
Code promo
</h3>
{!isPromoApplied ? (
<div className="flex gap-2">
<Input
placeholder="Entrez votre code"
value={promoCode}
onChange={(e) => setPromoCode(e.target.value)}
className="flex-1"
/>
<Button
onClick={applyPromoCode}
disabled={!promoCode.trim()}
variant="outline"
>
Appliquer
</Button>
</div>
) : (
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div className="flex items-center gap-2">
<Badge className="bg-green-500">✓</Badge>
<span className="text-sm font-medium">Code appliqué: {promoCode}</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={removePromoCode}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
)}
<div className="text-xs text-gray-500 space-y-1">
<p>Codes de démonstration disponibles:</p>
<p>• WELCOME10 (10% de réduction)</p>
<p>• SUMMER20 (20% de réduction)</p>
</div>
</CardContent>
</Card>
{/* Order Summary */}
<Card>
<CardContent className="p-6 space-y-4">
<h3 className="font-medium text-lg">Résumé de la commande</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span>Sous-total ({cart.itemCount} articles)</span>
<span>{formatPrice(subtotal)}</span>
</div>
<div className="flex justify-between">
<span className="flex items-center gap-1">
Livraison
{shipping === 0 && <Badge variant="secondary" className="text-xs">Gratuite</Badge>}
</span>
<span>{shipping === 0 ? 'Gratuite' : formatPrice(shipping)}</span>
</div>
{discount > 0 && (
<div className="flex justify-between text-green-600">
<span>Réduction</span>
<span>-{formatPrice(discount)}</span>
</div>
)}
<Separator />
<div className="flex justify-between text-lg font-medium">
<span>Total</span>
<span>{formatPrice(total)}</span>
</div>
</div>
<Button
asChild
size="lg"
className="w-full bg-black text-white hover:bg-gray-800 py-4"
>
<Link href="/checkout">
Procéder au paiement
<ArrowRight className="w-5 h-5 ml-2" />
</Link>
</Button>
{/* Trust Indicators */}
<div className="space-y-3 pt-4 border-t border-gray-200">
<div className="flex items-center gap-3 text-sm text-gray-600">
<Truck className="w-4 h-4" />
<span>Livraison gratuite à partir de 50.000 MRU</span>
</div>
<div className="flex items-center gap-3 text-sm text-gray-600">
<Shield className="w-4 h-4" />
<span>Paiement 100% sécurisé</span>
</div>
</div>
</CardContent>
</Card>
{/* Continue Shopping */}
<Button variant="outline" className="w-full" asChild>
<Link href="/boutique">
Continuer mes achats
</Link>
</Button>
</div>
</div>
</div>
</div>
);
}