mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-12 23:23:22 +00:00
only front-end init
This commit is contained in:
63
app/a-propos/page.tsx
Normal file
63
app/a-propos/page.tsx
Normal 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
56
app/admin/layout.tsx
Normal 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
67
app/admin/login/page.tsx
Normal 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
79
app/admin/page.tsx
Normal 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
277
app/admin/produits/page.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
162
app/admin/reservations/page.tsx
Normal file
162
app/admin/reservations/page.tsx
Normal 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
58
app/boutique/page.tsx
Normal 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
61
app/connexion/page.tsx
Normal 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
82
app/contact/page.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
138
app/globals.css
138
app/globals.css
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
169
app/page.tsx
169
app/page.tsx
@@ -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
77
app/panier/page.tsx
Normal 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
166
app/produit/[id]/page.tsx
Normal 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
141
app/reservation/page.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user