diff --git a/app/a-propos/page.tsx b/app/a-propos/page.tsx new file mode 100644 index 0000000..1213563 --- /dev/null +++ b/app/a-propos/page.tsx @@ -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 ( +
+ {/* Hero */} +
+
+ À propos +
+
+
+

{t("about.title")}

+
+
+ + {/* Story */} +
+

+ 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. +

+

+ 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é. +

+

+ 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. +

+
+ + {/* Values */} +
+
+
+
+ +

Passion

+

Chaque produit est choisi avec amour et expertise pour garantir votre satisfaction.

+
+
+ +

Qualité Premium

+

100% cheveux naturels Remy, sourcés éthiquement et contrôlés rigoureusement.

+
+
+ +

Expertise

+

Conseils personnalisés et pose professionnelle pour un résultat naturel.

+
+
+
+
+
+ ); +}; diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..1e77a43 --- /dev/null +++ b/app/admin/layout.tsx @@ -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 ( + //
+ //
Chargement...
+ //
+ // ); + // } + + if (isLoginRoute) { + return ( +
+
+
{children}
+
+
+ ); + } + + // Layout pour l'admin connecté (avec la sidebar) + return ( + +
+ +
+
+ +

+ Tableau de bord +

+
+
{children}
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx new file mode 100644 index 0000000..205a165 --- /dev/null +++ b/app/admin/login/page.tsx @@ -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 ( +
+ + +
+ +
+ Espace Admin + Accès réservé à la gestion du site +
+ +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + autoFocus + /> +

Démo : admin123

+
+ +
+
+
+
+ ); +}; diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..cedd0b8 --- /dev/null +++ b/app/admin/page.tsx @@ -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 ( +
+
+

Vue d'ensemble

+

Aperçu de votre activité

+
+ +
+ {stats.map((s) => ( + + +
+

{s.label}

+

{s.value}

+
+
+ +
+
+
+ ))} +
+ + + + Prochains rendez-vous + + + {upcoming.length === 0 ? ( +

Aucun rendez-vous à venir.

+ ) : ( +
+ {upcoming.map((r) => ( +
+
+

{r.clientName}

+

{r.service}

+
+
+

{new Date(r.date).toLocaleDateString("fr-FR", { day: "2-digit", month: "short" })} à {r.time}

+ + {r.status === "confirmed" ? "Confirmé" : "En attente"} + +
+
+ ))} +
+ )} +
+
+
+ ); +}; diff --git a/app/admin/produits/page.tsx b/app/admin/produits/page.tsx new file mode 100644 index 0000000..7834c57 --- /dev/null +++ b/app/admin/produits/page.tsx @@ -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(null); + const [form, setForm] = useState(emptyForm); + const [deleteId, setDeleteId] = useState(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 = { + "clip-in": "Clip-In", + "tape-in": "Tape-In", + "ponytail": "Ponytail", + "keratin": "Kératine", + }; + + return ( +
+
+
+

Produits

+

{products.length} produit{products.length > 1 ? "s" : ""} au catalogue

+
+ +
+ + + + + + Image + Nom + Catégorie + Prix + Statut + Actions + + + + {products.map((p) => ( + + + {p.name} + + {p.name} + {categoryLabels[p.category]} + {p.price} € + + {p.isNew && Nouveau} + {p.isBestseller && Bestseller} + + + + + + + ))} + +
+
+ + + + + {editingId ? "Modifier le produit" : "Nouveau produit"} + Renseignez les informations du produit + +
+
+
+ + setForm({ ...form, name: e.target.value })} required /> +
+
+ + +
+
+ + setForm({ ...form, price: e.target.value })} required /> +
+
+ + setForm({ ...form, image: e.target.value })} placeholder="https://..." /> +
+
+ +