mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-13 11:13:57 +00:00
Update May 12 by Elvis
This commit is contained in:
@@ -1,31 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } 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 { useAuth } from "@/contexts/AuthContext";
|
||||
import { listServices, formatDuration, ApiService } from "@/lib/api/services";
|
||||
import { getAvailableSlots, createBooking, TimeSlotApi } from "@/lib/api/bookings";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import { Check, Clock, CalendarDays } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function Booking () {
|
||||
const { t } = useLanguage();
|
||||
const [selectedService, setSelectedService] = useState<string>("");
|
||||
function toDateStr(d: Date): string {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export default function Booking() {
|
||||
const { t, locale } = useLanguage();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [services, setServices] = useState<ApiService[]>([]);
|
||||
const [servicesLoading, setServicesLoading] = useState(true);
|
||||
const [selectedService, setSelectedService] = useState<ApiService | null>(null);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>();
|
||||
const [selectedTime, setSelectedTime] = useState<string>("");
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [slots, setSlots] = useState<TimeSlotApi[]>([]);
|
||||
const [slotsLoading, setSlotsLoading] = useState(false);
|
||||
const [selectedSlot, setSelectedSlot] = useState<TimeSlotApi | null>(null);
|
||||
const [name, setName] = useState(user?.full_name ?? "");
|
||||
const [email, setEmail] = useState(user?.email ?? "");
|
||||
const [phone, setPhone] = useState(user?.phone ?? "");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [confirmed, setConfirmed] = useState(false);
|
||||
|
||||
const timeSlots = generateTimeSlots();
|
||||
const step = !selectedService ? 1 : !selectedDate ? 2 : !selectedTime ? 3 : 4;
|
||||
useEffect(() => {
|
||||
listServices()
|
||||
.then(setServices)
|
||||
.catch((e) => { console.error("[reservation] listServices failed:", e); setServices([]); })
|
||||
.finally(() => setServicesLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleConfirm = () => {
|
||||
toast.success(`${services.find((s) => s.id === selectedService)?.name} le ${selectedDate?.toLocaleDateString("fr-FR")} à ${selectedTime}`);
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setName(user.full_name ?? "");
|
||||
setEmail(user.email ?? "");
|
||||
setPhone(user.phone ?? "");
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDate) return;
|
||||
setSlotsLoading(true);
|
||||
setSelectedSlot(null);
|
||||
const dateStr = toDateStr(selectedDate);
|
||||
getAvailableSlots(dateStr, dateStr)
|
||||
.then(setSlots)
|
||||
.catch((e) => { console.error("[reservation] getAvailableSlots failed:", e); setSlots([]); })
|
||||
.finally(() => setSlotsLoading(false));
|
||||
}, [selectedDate]);
|
||||
|
||||
const step = !selectedService ? 1 : !selectedDate ? 2 : !selectedSlot ? 3 : 4;
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedSlot || !selectedService) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await createBooking({
|
||||
slot_id: selectedSlot.id,
|
||||
service_note: selectedService.name,
|
||||
...(user ? {} : { guest_name: name, guest_email: email, guest_phone: phone }),
|
||||
});
|
||||
setConfirmed(true);
|
||||
toast.success(t("booking.success"));
|
||||
} catch (err) {
|
||||
const msg = err instanceof ApiError ? err.message : t("booking.error");
|
||||
toast.error(msg);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (confirmed) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-6">
|
||||
<Check className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl mb-3">{t("booking.confirmed_title")}</h2>
|
||||
<p className="text-muted-foreground mb-2">
|
||||
{selectedService?.name} — {selectedDate?.toLocaleDateString(locale)} {t("booking.confirmed_at")} {selectedSlot?.start_time.slice(0, 5)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("booking.confirmed_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen py-8 lg:py-16">
|
||||
<div className="container mx-auto px-4 lg:px-8 max-w-4xl">
|
||||
@@ -35,42 +109,60 @@ export default function Booking () {
|
||||
<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
|
||||
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}
|
||||
{servicesLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-24 bg-muted animate-pulse rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : services.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{t("booking.no_services")}</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{services.map((service) => (
|
||||
<button
|
||||
key={service.id}
|
||||
onClick={() => setSelectedService(service)}
|
||||
className={`text-left p-4 rounded-lg border-2 transition-all cursor-pointer ${
|
||||
selectedService?.id === 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" />
|
||||
{formatDuration(service.duration_minutes)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-semibold">
|
||||
{service.price === 0 ? t("booking.free") : `${service.price} €`}
|
||||
</span>
|
||||
</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>
|
||||
{selectedService?.id === service.id && (
|
||||
<Check className="h-4 w-4 text-primary mt-2" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step 2: Date */}
|
||||
@@ -89,53 +181,94 @@ export default function Booking () {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Time */}
|
||||
{/* Step 3: Time slot */}
|
||||
{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>
|
||||
{slotsLoading ? (
|
||||
<div className="grid grid-cols-4 md:grid-cols-7 gap-2">
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<div key={i} className="h-10 bg-muted animate-pulse rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
) : slots.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{t("booking.no_slots")}</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-4 md:grid-cols-7 gap-2">
|
||||
{slots.map((slot) => (
|
||||
<button
|
||||
key={slot.id}
|
||||
onClick={() => setSelectedSlot(slot)}
|
||||
className={`py-2 px-3 rounded-md text-sm transition-colors cursor-pointer ${
|
||||
selectedSlot?.id === slot.id
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-foreground hover:bg-accent"
|
||||
}`}
|
||||
>
|
||||
{slot.start_time.slice(0, 5)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Contact info */}
|
||||
{selectedTime && (
|
||||
{/* Step 4: Contact */}
|
||||
{selectedSlot && (
|
||||
<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>
|
||||
{!user && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{user && (
|
||||
<div className="p-3 bg-muted rounded-lg text-sm">
|
||||
<p className="font-medium">{user.full_name}</p>
|
||||
<p className="text-muted-foreground">{user.email}</p>
|
||||
</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" />
|
||||
<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
|
||||
className="w-full"
|
||||
size="lg"
|
||||
onClick={handleConfirm}
|
||||
disabled={submitting || (!user && (!name || !email))}
|
||||
>
|
||||
{submitting ? t("booking.submitting") : t("booking.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user