252 lines
9.9 KiB
TypeScript
252 lines
9.9 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import { marketplacePost } from '@/lib/api'
|
||
import { useMarketplacePreferences } from '@/components/MarketplaceShell'
|
||
|
||
type Props = {
|
||
vehicleId: string
|
||
companySlug: string
|
||
dailyRate: number
|
||
}
|
||
|
||
const copy = {
|
||
en: {
|
||
title: 'Reserve this vehicle',
|
||
firstName: 'First name',
|
||
lastName: 'Last name',
|
||
email: 'Email',
|
||
phone: 'Phone',
|
||
startDate: 'Pick-up date',
|
||
endDate: 'Return date',
|
||
notes: 'Notes (optional)',
|
||
submit: 'Request reservation',
|
||
submitting: 'Sending…',
|
||
successTitle: 'Request received!',
|
||
successBody: 'Your reservation request has been sent. The company will review it and contact you shortly.',
|
||
invalidDates: 'Return date must be after pick-up date.',
|
||
required: 'Please fill in all required fields.',
|
||
perDay: '/ day',
|
||
estimatedTotal: 'Estimated total',
|
||
days: 'day(s)',
|
||
genericError: 'Something went wrong. Please try again.',
|
||
},
|
||
fr: {
|
||
title: 'Réserver ce véhicule',
|
||
firstName: 'Prénom',
|
||
lastName: 'Nom',
|
||
email: 'E-mail',
|
||
phone: 'Téléphone',
|
||
startDate: 'Date de départ',
|
||
endDate: 'Date de retour',
|
||
notes: 'Notes (optionnelles)',
|
||
submit: 'Demander une réservation',
|
||
submitting: 'Envoi…',
|
||
successTitle: 'Demande reçue !',
|
||
successBody: "Votre demande de réservation a été envoyée. L’entreprise va l’examiner et vous contactera prochainement.",
|
||
invalidDates: 'La date de retour doit être après la date de départ.',
|
||
required: 'Veuillez remplir tous les champs obligatoires.',
|
||
perDay: '/ jour',
|
||
estimatedTotal: 'Total estimé',
|
||
days: 'jour(s)',
|
||
genericError: 'Une erreur est survenue. Veuillez réessayer.',
|
||
},
|
||
ar: {
|
||
title: 'احجز هذه السيارة',
|
||
firstName: 'الاسم الأول',
|
||
lastName: 'اسم العائلة',
|
||
email: 'البريد الإلكتروني',
|
||
phone: 'الهاتف',
|
||
startDate: 'تاريخ الاستلام',
|
||
endDate: 'تاريخ الإرجاع',
|
||
notes: 'ملاحظات (اختيارية)',
|
||
submit: 'طلب حجز',
|
||
submitting: 'جارٍ الإرسال…',
|
||
successTitle: 'تم استلام الطلب!',
|
||
successBody: 'تم إرسال طلب الحجز. ستراجعه الشركة وستتواصل معك قريباً.',
|
||
invalidDates: 'يجب أن يكون تاريخ الإعادة بعد تاريخ الاستلام.',
|
||
required: 'يرجى تعبئة جميع الحقول المطلوبة.',
|
||
perDay: '/ يوم',
|
||
estimatedTotal: 'الإجمالي التقديري',
|
||
days: 'يوم',
|
||
genericError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
|
||
},
|
||
} as const
|
||
|
||
export default function BookingForm({ vehicleId, companySlug, dailyRate }: Props) {
|
||
const { language } = useMarketplacePreferences()
|
||
|
||
const t = copy[language]
|
||
|
||
const [firstName, setFirstName] = useState('')
|
||
const [lastName, setLastName] = useState('')
|
||
const [email, setEmail] = useState('')
|
||
const [phone, setPhone] = useState('')
|
||
const [startDate, setStartDate] = useState('')
|
||
const [endDate, setEndDate] = useState('')
|
||
const [notes, setNotes] = useState('')
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [success, setSuccess] = useState(false)
|
||
|
||
const totalDays =
|
||
startDate && endDate
|
||
? Math.max(0, Math.ceil((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86_400_000))
|
||
: 0
|
||
|
||
async function handleSubmit(e: React.FormEvent) {
|
||
e.preventDefault()
|
||
setError(null)
|
||
|
||
if (!firstName.trim() || !lastName.trim() || !email.trim() || !phone.trim() || !startDate || !endDate) {
|
||
setError(t.required)
|
||
return
|
||
}
|
||
|
||
const start = new Date(startDate)
|
||
const end = new Date(endDate)
|
||
if (end <= start) {
|
||
setError(t.invalidDates)
|
||
return
|
||
}
|
||
|
||
setSubmitting(true)
|
||
try {
|
||
await marketplacePost('/marketplace/reservations', {
|
||
vehicleId,
|
||
companySlug,
|
||
firstName: firstName.trim(),
|
||
lastName: lastName.trim(),
|
||
email: email.trim(),
|
||
phone: phone.trim(),
|
||
startDate: start.toISOString(),
|
||
endDate: end.toISOString(),
|
||
notes: notes.trim() || undefined,
|
||
language,
|
||
})
|
||
setSuccess(true)
|
||
} catch (err: any) {
|
||
setError(err.message ?? t.genericError)
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
if (success) {
|
||
return (
|
||
<div className="rounded-2xl border border-green-200 bg-green-50 p-6 dark:border-green-800 dark:bg-green-950/40">
|
||
<p className="text-base font-semibold text-green-800 dark:text-green-300">{t.successTitle}</p>
|
||
<p className="mt-2 text-sm text-green-700 dark:text-green-400">{t.successBody}</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<p className="text-sm font-semibold text-stone-900 dark:text-stone-100">{t.title}</p>
|
||
|
||
{error ? (
|
||
<p className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/40 dark:text-red-400">
|
||
{error}
|
||
</p>
|
||
) : null}
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.firstName} <span className="text-red-500">*</span></label>
|
||
<input
|
||
required
|
||
value={firstName}
|
||
onChange={(e) => setFirstName(e.target.value)}
|
||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.lastName} <span className="text-red-500">*</span></label>
|
||
<input
|
||
required
|
||
value={lastName}
|
||
onChange={(e) => setLastName(e.target.value)}
|
||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.email} <span className="text-red-500">*</span></label>
|
||
<input
|
||
required
|
||
type="email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.phone} <span className="text-red-500">*</span></label>
|
||
<input
|
||
required
|
||
type="tel"
|
||
value={phone}
|
||
onChange={(e) => setPhone(e.target.value)}
|
||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.startDate} <span className="text-red-500">*</span></label>
|
||
<input
|
||
required
|
||
type="date"
|
||
value={startDate}
|
||
min={new Date().toISOString().slice(0, 10)}
|
||
onChange={(e) => setStartDate(e.target.value)}
|
||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.endDate} <span className="text-red-500">*</span></label>
|
||
<input
|
||
required
|
||
type="date"
|
||
value={endDate}
|
||
min={startDate || new Date().toISOString().slice(0, 10)}
|
||
onChange={(e) => setEndDate(e.target.value)}
|
||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{totalDays > 0 ? (
|
||
<p className="text-sm text-stone-600 dark:text-stone-400">
|
||
{t.estimatedTotal}:{' '}
|
||
<strong className="text-stone-900 dark:text-stone-100">
|
||
{(dailyRate * totalDays).toLocaleString()} MAD
|
||
</strong>{' '}
|
||
({totalDays} {t.days})
|
||
</p>
|
||
) : null}
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.notes}</label>
|
||
<textarea
|
||
value={notes}
|
||
onChange={(e) => setNotes(e.target.value)}
|
||
rows={3}
|
||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={submitting}
|
||
className="w-full rounded-full bg-stone-900 px-6 py-3 text-center text-sm font-semibold text-white transition hover:bg-orange-700 disabled:opacity-60 dark:bg-orange-400 dark:text-white dark:hover:bg-orange-300"
|
||
>
|
||
{submitting ? t.submitting : t.submit}
|
||
</button>
|
||
</form>
|
||
)
|
||
}
|