only front-end init

This commit is contained in:
Rustico77
2026-05-05 21:48:23 +00:00
parent ac76a80c7b
commit b32a70cd0e
53 changed files with 11684 additions and 206 deletions

63
app/a-propos/page.tsx Normal file
View File

@@ -0,0 +1,63 @@
"use client";
import { useLanguage } from "@/contexts/LanguageContext";
import { Heart, Award, Sparkles } from "lucide-react";
export default function About() {
const { t } = useLanguage();
return (
<div className="min-h-screen">
{/* Hero */}
<section className="relative h-[50vh] flex items-center overflow-hidden">
<div className="absolute inset-0">
<img
src="https://images.unsplash.com/photo-1492106087820-71f1a00d2b11?w=1600&h=600&fit=crop"
alt="À propos"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-foreground/40" />
</div>
<div className="relative container mx-auto px-4 lg:px-8 text-center">
<h1 className="font-serif text-4xl lg:text-6xl text-background">{t("about.title")}</h1>
</div>
</section>
{/* Story */}
<section className="py-16 lg:py-24 container mx-auto px-4 lg:px-8 max-w-3xl">
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
Passionnée par la beauté et le bien-être capillaire depuis toujours, j'ai créé BADO HAIR pour offrir à chaque femme la possibilité de sublimer sa chevelure avec des extensions de qualité exceptionnelle.
</p>
<p className="text-lg text-muted-foreground leading-relaxed mb-6">
Chaque produit est sélectionné avec soin : des cheveux 100% naturels Remy, sourcés de manière éthique, traités avec les technologies les plus avancées pour garantir douceur, brillance et longévité.
</p>
<p className="text-lg text-muted-foreground leading-relaxed">
Mon objectif est simple : vous aider à vous sentir belle et confiante, que ce soit pour un événement spécial ou au quotidien. Chaque cliente mérite une expérience personnalisée et des conseils adaptés à ses besoins.
</p>
</section>
{/* Values */}
<section className="py-16 bg-muted/50">
<div className="container mx-auto px-4 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
<div className="text-center">
<Heart className="h-8 w-8 mx-auto mb-4 text-primary" />
<h3 className="font-serif text-lg mb-2">Passion</h3>
<p className="text-sm text-muted-foreground">Chaque produit est choisi avec amour et expertise pour garantir votre satisfaction.</p>
</div>
<div className="text-center">
<Award className="h-8 w-8 mx-auto mb-4 text-primary" />
<h3 className="font-serif text-lg mb-2">Qualité Premium</h3>
<p className="text-sm text-muted-foreground">100% cheveux naturels Remy, sourcés éthiquement et contrôlés rigoureusement.</p>
</div>
<div className="text-center">
<Sparkles className="h-8 w-8 mx-auto mb-4 text-primary" />
<h3 className="font-serif text-lg mb-2">Expertise</h3>
<p className="text-sm text-muted-foreground">Conseils personnalisés et pose professionnelle pour un résultat naturel.</p>
</div>
</div>
</div>
</section>
</div>
);
};

56
app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,56 @@
"use client";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AdminSidebar } from "@/components/admin/AdminSidebar";
import { useAdmin } from "@/contexts/AdminContext";
import { usePathname, useRouter } from "next/navigation";
import { useEffect } from "react";
export default function AdminLayout({ children }: { children: React.ReactNode }) {
const { isAdmin } = useAdmin();
const router = useRouter();
const pathname = usePathname();
const isLoginRoute = pathname === "/admin/login";
// useEffect(() => {
// if (!isAdmin && !isLoginRoute) {
// router.push("/admin/login");
// }
// }, [isAdmin, isLoginRoute, router]);
// if (!isAdmin && !isLoginRoute) {
// return (
// <div className="min-h-screen flex items-center justify-center bg-muted/30">
// <div className="text-center">Chargement...</div>
// </div>
// );
// }
if (isLoginRoute) {
return (
<div className="min-h-screen flex w-full bg-muted/30">
<div className="flex-1 flex flex-col">
<main className="flex-1 p-6">{children}</main>
</div>
</div>
);
}
// Layout pour l'admin connecté (avec la sidebar)
return (
<SidebarProvider>
<div className="min-h-screen flex w-full bg-muted/30">
<AdminSidebar />
<div className="flex-1 flex flex-col">
<header className="h-14 flex items-center border-b border-border bg-background px-4 sticky top-0 z-10">
<SidebarTrigger />
<h1 className="ml-4 text-sm font-medium text-muted-foreground">
Tableau de bord
</h1>
</header>
<main className="flex-1 p-6">{children}</main>
</div>
</div>
</SidebarProvider>
);
}

67
app/admin/login/page.tsx Normal file
View File

@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import { Lock } from "lucide-react";
import { useAdmin } from "@/contexts/AdminContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
export default function AdminLogin() {
const { login, isAdmin } = useAdmin();
const route = useRouter();
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setTimeout(() => {
const ok = login(password);
setLoading(false);
if (ok) {
toast.success("Connexion réussie");
route.push("/admin");
} else {
toast.error("Mot de passe incorrect");
}
}, 300);
};
return (
<div className="min-h-screen flex items-center justify-center bg-muted/30 px-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center mb-2">
<Lock className="h-5 w-5 text-primary" />
</div>
<CardTitle className="font-serif">Espace Admin</CardTitle>
<CardDescription>Accès réservé à la gestion du site</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
autoFocus
/>
<p className="text-xs text-muted-foreground">Démo : <span className="font-mono">admin123</span></p>
</div>
<Button type="submit" className="w-full p-6" disabled={loading}>
{loading ? "Connexion..." : "Se connecter"}
</Button>
</form>
</CardContent>
</Card>
</div>
);
};

79
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,79 @@
"use client";
import { Package, CalendarCheck, Clock, TrendingUp } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { useAdmin } from "@/contexts/AdminContext";
export default function AdminOverview() {
const { products, reservations } = useAdmin();
const pending = reservations.filter((r) => r.status === "pending").length;
const confirmed = reservations.filter((r) => r.status === "confirmed").length;
const totalValue = products.reduce((sum, p) => sum + p.price, 0);
const stats = [
{ label: "Produits", value: products.length, icon: Package, color: "text-blue-600", bg: "bg-blue-100" },
{ label: "RDV en attente", value: pending, icon: Clock, color: "text-amber-600", bg: "bg-amber-100" },
{ label: "RDV confirmés", value: confirmed, icon: CalendarCheck, color: "text-green-600", bg: "bg-green-100" },
{ label: "Valeur catalogue", value: `${totalValue}`, icon: TrendingUp, color: "text-primary", bg: "bg-primary/10" },
];
const upcoming = [...reservations]
.filter((r) => r.status !== "cancelled")
.sort((a, b) => `${a.date}${a.time}`.localeCompare(`${b.date}${b.time}`))
.slice(0, 5);
return (
<div className="space-y-6">
<div>
<h2 className="font-serif text-2xl font-semibold">Vue d'ensemble</h2>
<p className="text-sm text-muted-foreground mt-1">Aperçu de votre activité</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((s) => (
<Card key={s.label}>
<CardContent className="p-5 flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{s.label}</p>
<p className="text-2xl font-semibold mt-1">{s.value}</p>
</div>
<div className={`h-10 w-10 rounded-full ${s.bg} flex items-center justify-center`}>
<s.icon className={`h-5 w-5 ${s.color}`} />
</div>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">Prochains rendez-vous</CardTitle>
</CardHeader>
<CardContent>
{upcoming.length === 0 ? (
<p className="text-sm text-muted-foreground">Aucun rendez-vous à venir.</p>
) : (
<div className="space-y-3">
{upcoming.map((r) => (
<div key={r.id} className="flex items-center justify-between py-3 border-b border-border last:border-0">
<div>
<p className="font-medium text-sm">{r.clientName}</p>
<p className="text-xs text-muted-foreground">{r.service}</p>
</div>
<div className="text-right">
<p className="text-sm font-medium">{new Date(r.date).toLocaleDateString("fr-FR", { day: "2-digit", month: "short" })} à {r.time}</p>
<Badge variant={r.status === "confirmed" ? "default" : "secondary"} className="mt-1 text-xs">
{r.status === "confirmed" ? "Confirmé" : "En attente"}
</Badge>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
};

277
app/admin/produits/page.tsx Normal file
View File

@@ -0,0 +1,277 @@
"use client";
import { useState } from "react";
import { Plus, Pencil, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useAdmin } from "@/contexts/AdminContext";
import { Product } from "@/data/products";
import { toast } from "sonner";
type FormState = {
name: string;
category: Product["category"];
price: string;
image: string;
description: string;
colors: string;
lengths: string;
isNew: boolean;
isBestseller: boolean;
};
const emptyForm: FormState = {
name: "",
category: "clip-in",
price: "",
image: "",
description: "",
colors: "",
lengths: "",
isNew: false,
isBestseller: false,
};
export default function AdminProducts() {
const { products, addProduct, updateProduct, deleteProduct } = useAdmin();
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [form, setForm] = useState<FormState>(emptyForm);
const [deleteId, setDeleteId] = useState<string | null>(null);
const openCreate = () => {
setEditingId(null);
setForm(emptyForm);
setDialogOpen(true);
};
const openEdit = (p: Product) => {
setEditingId(p.id);
setForm({
name: p.name,
category: p.category,
price: String(p.price),
image: p.image,
description: p.description,
colors: p.colors.join(", "),
lengths: p.lengths.join(", "),
isNew: !!p.isNew,
isBestseller: !!p.isBestseller,
});
setDialogOpen(true);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const price = parseFloat(form.price);
if (!form.name || isNaN(price)) {
toast.error("Nom et prix valides requis");
return;
}
const payload = {
name: form.name,
category: form.category,
price,
image: form.image || "https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?w=600&h=800&fit=crop",
images: [form.image || "https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?w=600&h=800&fit=crop"],
colors: form.colors.split(",").map((c) => c.trim()).filter(Boolean),
lengths: form.lengths.split(",").map((l) => l.trim()).filter(Boolean),
description: form.description,
features: [],
isNew: form.isNew,
isBestseller: form.isBestseller,
rating: 5,
reviewCount: 0,
};
if (editingId) {
updateProduct(editingId, payload);
toast.success("Produit modifié");
} else {
addProduct(payload);
toast.success("Produit ajouté");
}
setDialogOpen(false);
};
const handleDelete = () => {
if (deleteId) {
deleteProduct(deleteId);
toast.success("Produit supprimé");
setDeleteId(null);
}
};
const categoryLabels: Record<Product["category"], string> = {
"clip-in": "Clip-In",
"tape-in": "Tape-In",
"ponytail": "Ponytail",
"keratin": "Kératine",
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="font-serif text-2xl font-semibold">Produits</h2>
<p className="text-sm text-muted-foreground mt-1">{products.length} produit{products.length > 1 ? "s" : ""} au catalogue</p>
</div>
<Button onClick={openCreate}>
<Plus className="h-4 w-4" />
Ajouter
</Button>
</div>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16">Image</TableHead>
<TableHead>Nom</TableHead>
<TableHead>Catégorie</TableHead>
<TableHead>Prix</TableHead>
<TableHead>Statut</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products.map((p) => (
<TableRow key={p.id}>
<TableCell>
<img src={p.image} alt={p.name} className="h-12 w-12 rounded object-cover" />
</TableCell>
<TableCell className="font-medium">{p.name}</TableCell>
<TableCell className="text-muted-foreground">{categoryLabels[p.category]}</TableCell>
<TableCell>{p.price} </TableCell>
<TableCell className="space-x-1">
{p.isNew && <Badge variant="secondary">Nouveau</Badge>}
{p.isBestseller && <Badge>Bestseller</Badge>}
</TableCell>
<TableCell className="text-right space-x-2">
<Button variant="ghost" size="icon" onClick={() => openEdit(p)}>
<Pencil className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => setDeleteId(p.id)}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="lg:min-w-2xl max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingId ? "Modifier le produit" : "Nouveau produit"}</DialogTitle>
<DialogDescription>Renseignez les informations du produit</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="name">Nom</Label>
<Input id="name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required />
</div>
<div className="space-y-2">
<Label>Catégorie</Label>
<Select value={form.category} onValueChange={(v) => setForm({ ...form, category: v as Product["category"] })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="clip-in">Clip-In</SelectItem>
<SelectItem value="tape-in">Tape-In</SelectItem>
<SelectItem value="ponytail">Ponytail</SelectItem>
<SelectItem value="keratin">Kératine</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="price">Prix ()</Label>
<Input id="price" type="number" step="0.01" value={form.price} onChange={(e) => setForm({ ...form, price: e.target.value })} required />
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="image">URL de l'image</Label>
<Input id="image" value={form.image} onChange={(e) => setForm({ ...form, image: e.target.value })} placeholder="https://..." />
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="description">Description</Label>
<Textarea id="description" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} rows={3} />
</div>
<div className="space-y-2">
<Label htmlFor="colors">Couleurs (séparées par des virgules)</Label>
<Input id="colors" value={form.colors} onChange={(e) => setForm({ ...form, colors: e.target.value })} placeholder="Brun, Blond, Noir" />
</div>
<div className="space-y-2">
<Label htmlFor="lengths">Longueurs (séparées par des virgules)</Label>
<Input id="lengths" value={form.lengths} onChange={(e) => setForm({ ...form, lengths: e.target.value })} placeholder="40 cm, 50 cm" />
</div>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={form.isNew} onChange={(e) => setForm({ ...form, isNew: e.target.checked })} />
Marquer comme Nouveau
</label>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={form.isBestseller} onChange={(e) => setForm({ ...form, isBestseller: e.target.checked })} />
Marquer comme Bestseller
</label>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>Annuler</Button>
<Button type="submit">{editingId ? "Enregistrer" : "Créer"}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<AlertDialog open={!!deleteId} onOpenChange={(o) => !o && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Supprimer ce produit ?</AlertDialogTitle>
<AlertDialogDescription>Cette action est irréversible.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>Supprimer</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};

View File

@@ -0,0 +1,162 @@
"use client";
import { useState } from "react";
import { Check, X, Trash2, Mail, Phone } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tabs,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useAdmin, Reservation } from "@/contexts/AdminContext";
import { toast } from "sonner";
const statusConfig: Record<Reservation["status"], { label: string; variant: "default" | "secondary" | "destructive" }> = {
pending: { label: "En attente", variant: "secondary" },
confirmed: { label: "Confirmé", variant: "default" },
cancelled: { label: "Annulé", variant: "destructive" },
};
export default function AdminReservations() {
const { reservations, updateReservationStatus, deleteReservation } = useAdmin();
const [filter, setFilter] = useState<"all" | Reservation["status"]>("all");
const [deleteId, setDeleteId] = useState<string | null>(null);
const filtered = reservations
.filter((r) => filter === "all" || r.status === filter)
.sort((a, b) => `${b.date}${b.time}`.localeCompare(`${a.date}${a.time}`));
const handleConfirm = (id: string) => {
updateReservationStatus(id, "confirmed");
toast.success("Réservation confirmée");
};
const handleCancel = (id: string) => {
updateReservationStatus(id, "cancelled");
toast.success("Réservation annulée");
};
const handleDelete = () => {
if (deleteId) {
deleteReservation(deleteId);
toast.success("Réservation supprimée");
setDeleteId(null);
}
};
return (
<div className="space-y-6">
<div>
<h2 className="font-serif text-2xl font-semibold">Réservations</h2>
<p className="text-sm text-muted-foreground mt-1">{reservations.length} réservation{reservations.length > 1 ? "s" : ""} au total</p>
</div>
<Tabs value={filter} onValueChange={(v) => setFilter(v as typeof filter)}>
<TabsList>
<TabsTrigger value="all">Toutes</TabsTrigger>
<TabsTrigger value="pending">En attente</TabsTrigger>
<TabsTrigger value="confirmed">Confirmées</TabsTrigger>
<TabsTrigger value="cancelled">Annulées</TabsTrigger>
</TabsList>
</Tabs>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Cliente</TableHead>
<TableHead>Contact</TableHead>
<TableHead>Service</TableHead>
<TableHead>Date & Heure</TableHead>
<TableHead>Statut</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
Aucune réservation
</TableCell>
</TableRow>
) : (
filtered.map((r) => (
<TableRow key={r.id}>
<TableCell className="font-medium">{r.clientName}</TableCell>
<TableCell>
<div className="text-xs space-y-1">
<div className="flex items-center gap-1 text-muted-foreground">
<Mail className="h-3 w-3" /> {r.email}
</div>
<div className="flex items-center gap-1 text-muted-foreground">
<Phone className="h-3 w-3" /> {r.phone}
</div>
</div>
</TableCell>
<TableCell>{r.service}</TableCell>
<TableCell>
<div className="text-sm">
{new Date(r.date).toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "numeric" })}
</div>
<div className="text-xs text-muted-foreground">{r.time}</div>
</TableCell>
<TableCell>
<Badge variant={statusConfig[r.status].variant}>{statusConfig[r.status].label}</Badge>
</TableCell>
<TableCell className="text-right space-x-1">
{r.status !== "confirmed" && (
<Button variant="ghost" size="icon" onClick={() => handleConfirm(r.id)} title="Confirmer">
<Check className="h-4 w-4 text-primary" />
</Button>
)}
{r.status !== "cancelled" && (
<Button variant="ghost" size="icon" onClick={() => handleCancel(r.id)} title="Annuler">
<X className="h-4 w-4 text-muted-foreground" />
</Button>
)}
<Button variant="ghost" size="icon" onClick={() => setDeleteId(r.id)} title="Supprimer">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
<AlertDialog open={!!deleteId} onOpenChange={(o) => !o && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Supprimer cette réservation ?</AlertDialogTitle>
<AlertDialogDescription>Cette action est irréversible.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>Supprimer</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};

58
app/boutique/page.tsx Normal file
View File

@@ -0,0 +1,58 @@
"use client";
import { useState } from "react";
import { useLanguage } from "@/contexts/LanguageContext";
import { products, categories } from "@/data/products";
import ProductCard from "@/components/ProductCard";
export default function Shop() {
const { t } = useLanguage();
// const [searchParams] = useSearchParams();
// const initialCategory = searchParams.get("category") || "all";
const [selectedCategory, setSelectedCategory] = useState("all");
const filtered = selectedCategory === "all" ? products : products.filter((p) => p.category === selectedCategory);
return (
<div className="min-h-screen py-20 lg:py-12">
<div className="container mx-auto px-4 lg:px-8">
<h1 className="font-serif text-3xl lg:text-5xl text-center mb-8">{t("shop.title")}</h1>
{/* Filters */}
<div className="flex flex-wrap justify-center gap-2 mb-10">
<button
onClick={() => setSelectedCategory("all")}
className={`px-4 py-2 rounded-full text-sm transition-colors ${
selectedCategory === "all" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-accent"
}`}
>
{t("shop.filter.all")}
</button>
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className={`px-4 py-2 rounded-full text-sm transition-colors cursor-pointer ${
selectedCategory === cat.id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-accent"
}`}
>
{cat.name}
</button>
))}
</div>
{/* Product Grid */}
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
{filtered.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
{filtered.length === 0 && (
<p className="text-center text-muted-foreground py-20">Aucun produit trouvé.</p>
)}
</div>
</div>
);
};

61
app/connexion/page.tsx Normal file
View File

@@ -0,0 +1,61 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useLanguage } from "@/contexts/LanguageContext";
import { toast } from "sonner";
export default function Auth() {
const { t } = useLanguage();
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
toast.success(isLogin ? "Connexion réussie" : "Compte créé avec succès !");
};
return (
<div className="min-h-screen flex items-center justify-center py-12 px-4">
<div className="w-full max-w-sm">
<h1 className="font-serif text-3xl text-center mb-2">
{isLogin ? t("auth.login") : t("auth.register")}
</h1>
<p className="text-sm text-muted-foreground text-center mb-8">
{isLogin ? "Accédez à votre espace personnel" : "Créez votre compte en quelques secondes"}
</p>
<form onSubmit={handleSubmit} className="space-y-4">
{!isLogin && (
<div>
<Label htmlFor="name">{t("auth.name")}</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} className="mt-1" />
</div>
)}
<div>
<Label htmlFor="email">{t("auth.email")}</Label>
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1" />
</div>
<div>
<Label htmlFor="password">{t("auth.password")}</Label>
<Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="mt-1" />
</div>
<Button type="submit" className="w-full" size="lg">
{isLogin ? t("auth.login") : t("auth.register")}
</Button>
</form>
<p className="text-center text-sm text-muted-foreground mt-6">
{isLogin ? "Pas encore de compte ?" : "Déjà un compte ?"}{" "}
<Button variant="link" onClick={() => setIsLogin(!isLogin)} className="text-primary hover:underline font-medium">
{isLogin ? t("auth.register") : t("auth.login")}
</Button>
</p>
</div>
</div>
);
};

82
app/contact/page.tsx Normal file
View File

@@ -0,0 +1,82 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { useLanguage } from "@/contexts/LanguageContext";
import { MapPin, Phone, Mail } from "lucide-react";
import { toast } from "sonner";
export default function Contact() {
const { t } = useLanguage();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
toast.success("Nous vous répondrons dans les plus brefs délais.");
setName("");
setEmail("");
setMessage("");
};
return (
<div className="min-h-screen py-12 lg:py-20">
<div className="container mx-auto px-4 lg:px-8 max-w-5xl">
<h1 className="font-serif text-3xl lg:text-5xl text-center mb-12">{t("contact.title")}</h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Info */}
<div className="space-y-8">
<p className="text-muted-foreground leading-relaxed">
Une question sur nos produits, un conseil personnalisé ou une demande de rendez-vous ? N'hésitez pas à nous contacter.
</p>
<div className="space-y-4">
<div className="flex items-center gap-3">
<MapPin className="h-5 w-5 text-primary" />
<span className="text-sm">123 Rue de la Beauté, 75001 Paris</span>
</div>
<div className="flex items-center gap-3">
<Phone className="h-5 w-5 text-primary" />
<span className="text-sm">+33 1 23 45 67 89</span>
</div>
<div className="flex items-center gap-3">
<Mail className="h-5 w-5 text-primary" />
<span className="text-sm">contact@luxehair.com</span>
</div>
</div>
<div className="text-sm text-muted-foreground">
<h3 className="font-medium text-foreground mb-2">Horaires d'ouverture</h3>
<p>Lundi - Vendredi : 9h - 18h</p>
<p>Samedi : 10h - 16h</p>
<p>Dimanche : Fermé</p>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="name">{t("auth.name")}</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required />
</div>
<div>
<Label htmlFor="email">{t("auth.email")}</Label>
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1" required />
</div>
<div>
<Label htmlFor="message">{t("contact.message")}</Label>
<Textarea id="message" rows={5} value={message} onChange={(e) => setMessage(e.target.value)} className="mt-1" required />
</div>
<Button type="submit" className="w-full" size="lg">
{t("contact.send")}
</Button>
</form>
</div>
</div>
</div>
);
};

View File

@@ -1,26 +1,134 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
:root {
--background: #ffffff;
--foreground: #171717;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: #723e3c;
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
font-family: "Inter", sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Playfair Display', serif;
}
html {
@apply font-sans;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,33 +1,43 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
// app/layout.tsx
'use client';
import { usePathname } from 'next/navigation';
import { LanguageProvider } from "@/contexts/LanguageContext";
import { AdminProvider } from "@/contexts/AdminContext";
import { CartProvider } from "@/contexts/CartContext";
import CartDrawer from "@/components/CartDrawer";
import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { Toaster } from "sonner";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Vérifier si on est sur une route admin
const isAdminRoute = pathname?.startsWith('/admin');
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<html>
<body>
<LanguageProvider>
<AdminProvider>
<CartProvider>
<Toaster position="top-right" richColors />
{/* Afficher Header seulement si ce n'est pas l'admin */}
{!isAdminRoute && <Header />}
{!isAdminRoute && <CartDrawer />}
{children}
{/* Afficher Footer seulement si ce n'est pas l'admin */}
{!isAdminRoute && <Footer />}
</CartProvider>
</AdminProvider>
</LanguageProvider>
</body>
</html>
);
}
}

View File

@@ -1,65 +1,120 @@
import Image from "next/image";
"use client";
import { ArrowRight, Star, Calendar } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useLanguage } from "@/contexts/LanguageContext";
import { products, categories, reviews } from "@/data/products";
import ProductCard from "@/components/ProductCard";
import Link from "next/link";
export default function Home() {
const { t } = useLanguage();
const bestsellers = products.filter((p) => p.isBestseller);
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<div className="min-h-screen">
{/* Hero */}
<section className="relative h-[85vh] flex items-center overflow-hidden">
<div className="absolute inset-0">
<img
src="https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=1600&h=900&fit=crop"
alt="Extensions de cheveux luxe"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-r from-foreground/60 via-foreground/30 to-transparent" />
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
<div className="relative container mx-auto px-4 lg:px-8">
<div className="max-w-xl">
<h1 className="font-serif text-4xl lg:text-6xl font-semibold text-background leading-tight mb-4">
{t("hero.title")}
</h1>
<p className="text-background/80 text-lg mb-8">
{t("hero.subtitle")}
</p>
<Link href="/boutique">
<Button size="lg" className="group text-sm tracking-wide">
{t("hero.cta")}
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
</div>
</main>
</section>
{/* Categories */}
<section className="py-16 lg:py-24 container mx-auto px-4 lg:px-8">
<h2 className="font-serif text-3xl lg:text-4xl text-center mb-12">{t("categories.title")}</h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
{categories.map((cat) => (
<Link key={cat.id} href={`/boutique?category=${cat.id}`} className="group relative overflow-hidden rounded-lg aspect-[3/4]">
<img src={cat.image} alt={cat.name} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy" />
<div className="absolute inset-0 bg-gradient-to-t from-foreground/70 to-transparent" />
<div className="absolute bottom-4 left-4 text-background">
<h3 className="font-serif text-lg font-semibold">{cat.name}</h3>
<p className="text-xs opacity-80">{cat.description}</p>
</div>
</Link>
))}
</div>
</section>
{/* Bestsellers */}
<section className="py-16 lg:py-24 bg-muted/50">
<div className="container mx-auto px-4 lg:px-8">
<div className="flex items-center justify-between mb-10">
<h2 className="font-serif text-3xl lg:text-4xl">{t("bestsellers.title")}</h2>
<Link href="/boutique" className="text-sm text-primary hover:underline flex items-center gap-1">
{t("nav.shop")} <ArrowRight className="h-3 w-3" />
</Link>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
{bestsellers.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</div>
</section>
{/* Booking Banner */}
<section className="relative py-20 lg:py-32 overflow-hidden">
<div className="absolute inset-0">
<img
src="https://images.unsplash.com/photo-1580618672591-eb180b1a973f?w=1600&h=600&fit=crop"
alt="Réservation"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-foreground/50" />
</div>
<div className="relative container mx-auto px-4 lg:px-8 text-center">
<Calendar className="h-10 w-10 text-background/80 mx-auto mb-4" />
<h2 className="font-serif text-3xl lg:text-5xl text-background mb-3">{t("booking.title")}</h2>
<p className="text-background/70 mb-8 text-lg">{t("booking.subtitle")}</p>
<Link href="/reservation">
<Button variant="outline" className="border-background p-6 text-lg cursor-pointer text-foreground hover:bg-primary hover:text-background transition-colors">
{t("booking.cta")}
</Button>
</Link>
</div>
</section>
{/* Reviews */}
<section className="py-16 lg:py-24 container mx-auto px-4 lg:px-8">
<h2 className="font-serif text-3xl lg:text-4xl text-center mb-12">{t("reviews.title")}</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{reviews.map((review) => (
<div key={review.id} className="bg-card rounded-lg p-6 border border-border">
<div className="flex gap-0.5 mb-3">
{Array.from({ length: review.rating }).map((_, i) => (
<Star key={i} className="h-4 w-4 fill-primary text-primary" />
))}
</div>
<p className="text-sm text-muted-foreground leading-relaxed mb-4">{review.text}</p>
<p className="text-sm font-medium">{review.name}</p>
</div>
))}
</div>
</section>
</div>
);
}

77
app/panier/page.tsx Normal file
View File

@@ -0,0 +1,77 @@
"use client";
import { Minus, Plus, X, ArrowLeft, ShoppingBag } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useCart } from "@/contexts/CartContext";
import { useLanguage } from "@/contexts/LanguageContext";
import Link from "next/link";
export default function Cart() {
const { items, updateQuantity, removeItem, totalPrice } = useCart();
const { t } = useLanguage();
return (
<div className="min-h-screen py-8 lg:py-12">
<div className="container mx-auto px-4 lg:px-8 max-w-3xl">
<h1 className="font-serif text-3xl lg:text-4xl text-center mb-10">{t("cart.title")}</h1>
{items.length === 0 ? (
<div className="text-center py-20">
<ShoppingBag className="h-16 w-16 mx-auto mb-4 text-muted-foreground/30" />
<p className="text-muted-foreground mb-6">{t("cart.empty")}</p>
<Link href="/boutique">
<Button variant="outline">
<ArrowLeft className="h-4 w-4 mr-2" />
{t("nav.shop")}
</Button>
</Link>
</div>
) : (
<>
<div className="space-y-4">
{items.map((item) => (
<div key={`${item.product.id}-${item.selectedColor}-${item.selectedLength}`} className="flex gap-4 p-4 bg-card rounded-lg border border-border">
<img src={item.product.image} alt={item.product.name} className="w-24 h-32 object-cover rounded-md" />
<div className="flex-1">
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium">{item.product.name}</h3>
<p className="text-sm text-muted-foreground mt-0.5">{item.selectedColor} {item.selectedLength}</p>
</div>
<button onClick={() => removeItem(item.product.id)} className="text-muted-foreground hover:text-destructive">
<X className="h-4 w-4" />
</button>
</div>
<div className="flex items-center justify-between mt-4">
<div className="flex items-center gap-3">
<button onClick={() => updateQuantity(item.product.id, item.quantity - 1)} className="p-1.5 border border-border rounded-md hover:bg-muted">
<Minus className="h-3 w-3" />
</button>
<span className="text-sm font-medium w-6 text-center">{item.quantity}</span>
<button onClick={() => updateQuantity(item.product.id, item.quantity + 1)} className="p-1.5 border border-border rounded-md hover:bg-muted">
<Plus className="h-3 w-3" />
</button>
</div>
<span className="font-semibold">{(item.product.price * item.quantity).toFixed(2)} </span>
</div>
</div>
</div>
))}
</div>
<div className="mt-8 bg-card rounded-lg border border-border p-6">
<div className="flex justify-between items-center text-lg mb-4">
<span className="font-medium">{t("cart.total")}</span>
<span className="font-serif text-xl font-semibold">{totalPrice.toFixed(2)} </span>
</div>
<Button className="w-full" size="lg">
{t("cart.checkout")}
</Button>
<p className="text-xs text-muted-foreground text-center mt-3">Paiement sécurisé par Stripe</p>
</div>
</>
)}
</div>
</div>
);
};

166
app/produit/[id]/page.tsx Normal file
View File

@@ -0,0 +1,166 @@
"use client";
import { useState } from "react";
import { Star, ChevronLeft, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useLanguage } from "@/contexts/LanguageContext";
import { useCart } from "@/contexts/CartContext";
import { products } from "@/data/products";
import ProductCard from "@/components/ProductCard";
import Link from "next/link";
import { useParams } from "next/navigation";
import { toast } from "sonner";
export default function ProductDetail() {
const { id } = useParams();
const { t } = useLanguage();
const { addItem } = useCart();
const product = products.find((p) => p.id === id);
const [selectedImage, setSelectedImage] = useState(0);
const [selectedColor, setSelectedColor] = useState("");
const [selectedLength, setSelectedLength] = useState("");
if (!product) {
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-muted-foreground">Produit non trouvé</p>
</div>
);
}
const similar = products.filter((p) => p.category === product.category && p.id !== product.id).slice(0, 4);
const handleAddToCart = () => {
const color = selectedColor || product.colors[0];
const length = selectedLength || product.lengths[0];
addItem(product, color, length);
toast.success(`Ajouté au panier - ${product.name} - ${color}, ${length}`);
};
return (
<div className="min-h-screen py-6 lg:py-12">
<div className="container mx-auto px-4 lg:px-8">
<Link href="/boutique" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-6">
<ChevronLeft className="h-4 w-4" /> {t("nav.shop")}
</Link>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
{/* Images */}
<div className="space-y-3">
<div className="aspect-[3/4] rounded-lg overflow-hidden bg-muted">
<img src={product.images[selectedImage]} alt={product.name} className="w-full h-full object-cover" />
</div>
{product.images.length > 1 && (
<div className="flex gap-2">
{product.images.map((img, i) => (
<button
key={i}
onClick={() => setSelectedImage(i)}
className={`w-16 h-20 rounded-md overflow-hidden border-2 transition-colors ${
i === selectedImage ? "border-primary" : "border-transparent"
}`}
>
<img src={img} alt="" className="w-full h-full object-cover" />
</button>
))}
</div>
)}
</div>
{/* Details */}
<div className="space-y-6">
<div>
<h1 className="font-serif text-2xl lg:text-4xl font-semibold mb-2">{product.name}</h1>
<div className="flex items-center gap-2 mb-4">
<div className="flex gap-0.5">
{Array.from({ length: 5 }).map((_, i) => (
<Star key={i} className={`h-4 w-4 ${i < Math.floor(product.rating) ? "fill-primary text-primary" : "text-muted"}`} />
))}
</div>
<span className="text-sm text-muted-foreground">({product.reviewCount} avis)</span>
</div>
<div className="flex items-baseline gap-3">
<span className="text-2xl font-semibold">{product.price} </span>
{product.originalPrice && (
<span className="text-lg text-muted-foreground line-through">{product.originalPrice} </span>
)}
</div>
</div>
<p className="text-muted-foreground leading-relaxed">{product.description}</p>
{/* Color selector */}
<div>
<h3 className="text-sm font-medium mb-2">{t("product.color")}</h3>
<div className="flex flex-wrap gap-2">
{product.colors.map((color) => (
<button
key={color}
onClick={() => setSelectedColor(color)}
className={`px-3 py-1.5 rounded-full text-sm border transition-colors cursor-pointer ${
(selectedColor || product.colors[0]) === color
? "border-primary bg-accent text-accent-foreground"
: "border-border text-muted-foreground hover:border-primary"
}`}
>
{color}
</button>
))}
</div>
</div>
{/* Length selector */}
<div>
<h3 className="text-sm font-medium mb-2">{t("product.length")}</h3>
<div className="flex gap-2">
{product.lengths.map((length) => (
<button
key={length}
onClick={() => setSelectedLength(length)}
className={`px-4 py-2 rounded-md text-sm border transition-colors cursor-pointer ${
(selectedLength || product.lengths[0]) === length
? "border-primary bg-accent text-accent-foreground"
: "border-border text-muted-foreground hover:border-primary"
}`}
>
{length}
</button>
))}
</div>
</div>
<Button size="lg" className="w-full text-sm tracking-wide" onClick={handleAddToCart}>
{t("cart.add")}
</Button>
{/* Features */}
<div>
<h3 className="text-sm font-medium mb-3">{t("product.features")}</h3>
<ul className="space-y-2">
{product.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-muted-foreground">
<Check className="h-4 w-4 text-primary" />
{feature}
</li>
))}
</ul>
</div>
</div>
</div>
{/* Similar */}
{similar.length > 0 && (
<section className="mt-16 lg:mt-24">
<h2 className="font-serif text-2xl mb-8">{t("product.similar")}</h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
{similar.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</section>
)}
</div>
</div>
);
};

141
app/reservation/page.tsx Normal file
View File

@@ -0,0 +1,141 @@
"use client";
import { useState } from "react";
import { Calendar } from "@/components/ui/calendar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useLanguage } from "@/contexts/LanguageContext";
import { services, generateTimeSlots } from "@/data/services";
import { Check, Clock, CalendarDays } from "lucide-react";
import { toast } from "sonner";
export default function Booking () {
const { t } = useLanguage();
const [selectedService, setSelectedService] = useState<string>("");
const [selectedDate, setSelectedDate] = useState<Date | undefined>();
const [selectedTime, setSelectedTime] = useState<string>("");
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const timeSlots = generateTimeSlots();
const step = !selectedService ? 1 : !selectedDate ? 2 : !selectedTime ? 3 : 4;
const handleConfirm = () => {
toast.success(`${services.find((s) => s.id === selectedService)?.name} le ${selectedDate?.toLocaleDateString("fr-FR")} à ${selectedTime}`);
};
return (
<div className="min-h-screen py-8 lg:py-16">
<div className="container mx-auto px-4 lg:px-8 max-w-4xl">
<div className="text-center mb-12">
<CalendarDays className="h-10 w-10 mx-auto mb-4 text-primary" />
<h1 className="font-serif text-3xl lg:text-5xl mb-3">{t("booking.title")}</h1>
<p className="text-muted-foreground">{t("booking.subtitle")}</p>
</div>
{/* Progress steps */}
<div className="flex justify-center gap-2 mb-10">
{[1, 2, 3, 4].map((s) => (
<div key={s} className={`h-1.5 w-12 rounded-full transition-colors ${s <= step ? "bg-primary" : "bg-muted"}`} />
))}
</div>
{/* Step 1: Service */}
<div className="mb-10">
<h2 className="font-serif text-xl mb-4">{t("booking.select_service")}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{services.map((service) => (
<button
key={service.id}
onClick={() => setSelectedService(service.id)}
className={`text-left p-4 rounded-lg border-2 transition-all cursor-pointer ${
selectedService === service.id ? "border-primary" : "border-border hover:border-primary/40"
}`}
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium text-sm">{service.name}</h3>
<p className="text-xs text-muted-foreground mt-1">{service.description}</p>
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{service.duration}
</div>
</div>
<span className="text-sm font-semibold">
{service.price === 0 ? t("booking.free") : `${service.price}`}
</span>
</div>
{selectedService === service.id && <Check className="h-4 w-4 text-primary mt-2" />}
</button>
))}
</div>
</div>
{/* Step 2: Date */}
{selectedService && (
<div className="mb-10">
<h2 className="font-serif text-xl mb-4">{t("booking.select_date")}</h2>
<div className="flex justify-center">
<Calendar
mode="single"
selected={selectedDate}
onSelect={setSelectedDate}
disabled={(date) => date < new Date() || date.getDay() === 0}
className="rounded-lg border border-border p-4"
/>
</div>
</div>
)}
{/* Step 3: Time */}
{selectedDate && (
<div className="mb-10">
<h2 className="font-serif text-xl mb-4">{t("booking.select_time")}</h2>
<div className="grid grid-cols-4 md:grid-cols-7 gap-2">
{timeSlots.map((slot) => (
<button
key={slot.time}
onClick={() => slot.available && setSelectedTime(slot.time)}
disabled={!slot.available}
className={`py-2 px-3 rounded-md text-sm transition-colors cursor-pointer ${
selectedTime === slot.time
? "bg-primary text-primary-foreground"
: slot.available
? "bg-muted text-foreground hover:bg-accent"
: "bg-muted/50 text-muted-foreground/30 cursor-not-allowed"
}`}
>
{slot.time}
</button>
))}
</div>
</div>
)}
{/* Step 4: Contact info */}
{selectedTime && (
<div className="max-w-md mx-auto space-y-4">
<h2 className="font-serif text-xl mb-4 text-center">{t("booking.confirm")}</h2>
<div>
<Label htmlFor="name">{t("auth.name")}</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} className="mt-1" />
</div>
<div>
<Label htmlFor="email">{t("auth.email")}</Label>
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1" />
</div>
<div>
<Label htmlFor="phone">{t("booking.phone")}</Label>
<Input id="phone" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} className="mt-1" />
</div>
<Button className="w-full" size="lg" onClick={handleConfirm} disabled={!name || !email || !phone}>
{t("booking.confirm")}
</Button>
</div>
)}
</div>
</div>
);
};