move to gitea
This commit is contained in:
234
src_backup/app/boutique/page.tsx
Normal file
234
src_backup/app/boutique/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
529
src_backup/app/checkout/page.tsx
Normal file
529
src_backup/app/checkout/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
227
src_backup/app/checkout/success/page.tsx
Normal file
227
src_backup/app/checkout/success/page.tsx
Normal 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
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
430
src_backup/app/globals.css
Normal 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
222
src_backup/app/layout.tsx
Normal 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
214
src_backup/app/page.tsx
Normal 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'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'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>
|
||||
);
|
||||
}
|
||||
322
src_backup/app/panier/page.tsx
Normal file
322
src_backup/app/panier/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user