dashboard fix
This commit is contained in:
@@ -140,7 +140,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
email: form.email,
|
||||
phone: form.phone || undefined,
|
||||
phone: form.phone,
|
||||
startDate: new Date(`${form.startDate}T${form.startTime}`).toISOString(),
|
||||
endDate: new Date(`${form.endDate}T${form.endTime}`).toISOString(),
|
||||
notes: form.notes || undefined,
|
||||
@@ -267,7 +267,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
{/* Dates + Times */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.pickupDate}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.pickupDate} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
@@ -278,7 +278,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.returnDate}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.returnDate} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
@@ -289,7 +289,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.pickupTime}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.pickupTime} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="time"
|
||||
required
|
||||
@@ -299,7 +299,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.dropoffTime}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.dropoffTime} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="time"
|
||||
required
|
||||
@@ -321,7 +321,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
{/* Customer info */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.firstName}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.firstName} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -331,7 +331,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.lastName}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.lastName} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -343,7 +343,7 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.email}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.email} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
@@ -354,9 +354,10 @@ export default function ExploreVehicleGrid({ vehicles, dict }: { vehicles: Vehic
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.phone}</label>
|
||||
<label className="block text-xs font-medium text-stone-600 mb-1">{dict.phone} <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={form.phone}
|
||||
onChange={set('phone')}
|
||||
className="w-full rounded-xl border border-stone-200 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-stone-900"
|
||||
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link'
|
||||
import { marketplaceFetchOrDefault } from '@/lib/api'
|
||||
import { getMarketplaceLanguage } from '@/lib/i18n.server'
|
||||
import { formatCurrency } from '@rentaldrivego/types'
|
||||
import BookingForm from '@/components/BookingForm'
|
||||
|
||||
interface VehicleDetail {
|
||||
id: string
|
||||
@@ -32,43 +33,37 @@ export default async function VehicleDetailPage({ params }: { params: { slug: st
|
||||
const language = getMarketplaceLanguage()
|
||||
const dict = {
|
||||
en: {
|
||||
unavailable: 'Marketplace unavailable',
|
||||
unavailableTitle: 'Vehicle details are temporarily unavailable.',
|
||||
unavailableBody: 'The marketplace frontend is running, but the backing database is not reachable right now.',
|
||||
backToFleet: 'Back to company fleet',
|
||||
noPhotos: 'No photos available.',
|
||||
rentalCompany: 'Rental company',
|
||||
perDay: '/ day',
|
||||
continueTo: 'Continue to',
|
||||
bookingSite: 'booking site',
|
||||
availableFrom: 'Available from',
|
||||
unavailableNoDate: 'This vehicle is temporarily unavailable for new reservations.',
|
||||
unavailable: ‘Marketplace unavailable’,
|
||||
unavailableTitle: ‘Vehicle details are temporarily unavailable.’,
|
||||
unavailableBody: ‘The marketplace frontend is running, but the backing database is not reachable right now.’,
|
||||
backToFleet: ‘Back to company fleet’,
|
||||
noPhotos: ‘No photos available.’,
|
||||
rentalCompany: ‘Rental company’,
|
||||
perDay: ‘/ day’,
|
||||
availableFrom: ‘Available from’,
|
||||
unavailableNoDate: ‘This vehicle is temporarily unavailable for new reservations.’,
|
||||
},
|
||||
fr: {
|
||||
unavailable: 'Marketplace indisponible',
|
||||
unavailableTitle: 'Les détails du véhicule sont temporairement indisponibles.',
|
||||
unavailableBody: 'Le frontend marketplace fonctionne, mais la base de données n’est pas accessible pour le moment.',
|
||||
backToFleet: 'Retour à la flotte',
|
||||
noPhotos: 'Aucune photo disponible.',
|
||||
rentalCompany: 'Société de location',
|
||||
perDay: '/ jour',
|
||||
continueTo: 'Continuer vers le site de réservation de',
|
||||
bookingSite: '',
|
||||
availableFrom: 'Disponible à partir du',
|
||||
unavailableNoDate: 'Ce véhicule est temporairement indisponible pour les nouvelles réservations.',
|
||||
unavailable: ‘Marketplace indisponible’,
|
||||
unavailableTitle: ‘Les détails du véhicule sont temporairement indisponibles.’,
|
||||
unavailableBody: ‘Le frontend marketplace fonctionne, mais la base de données n’est pas accessible pour le moment.’,
|
||||
backToFleet: ‘Retour à la flotte’,
|
||||
noPhotos: ‘Aucune photo disponible.’,
|
||||
rentalCompany: ‘Société de location’,
|
||||
perDay: ‘/ jour’,
|
||||
availableFrom: ‘Disponible à partir du’,
|
||||
unavailableNoDate: ‘Ce véhicule est temporairement indisponible pour les nouvelles réservations.’,
|
||||
},
|
||||
ar: {
|
||||
unavailable: 'السوق غير متاح',
|
||||
unavailableTitle: 'تفاصيل السيارة غير متاحة مؤقتاً.',
|
||||
unavailableBody: 'واجهة السوق تعمل، لكن قاعدة البيانات الخلفية غير متاحة حالياً.',
|
||||
backToFleet: 'العودة إلى أسطول الشركة',
|
||||
noPhotos: 'لا توجد صور متاحة.',
|
||||
rentalCompany: 'شركة تأجير',
|
||||
perDay: '/ يوم',
|
||||
continueTo: 'الانتقال إلى موقع حجز',
|
||||
bookingSite: '',
|
||||
availableFrom: 'متاح ابتداءً من',
|
||||
unavailableNoDate: 'هذه السيارة غير متاحة مؤقتاً للحجوزات الجديدة.',
|
||||
unavailable: ‘السوق غير متاح’,
|
||||
unavailableTitle: ‘تفاصيل السيارة غير متاحة مؤقتاً.’,
|
||||
unavailableBody: ‘واجهة السوق تعمل، لكن قاعدة البيانات الخلفية غير متاحة حالياً.’,
|
||||
backToFleet: ‘العودة إلى أسطول الشركة’,
|
||||
noPhotos: ‘لا توجد صور متاحة.’,
|
||||
rentalCompany: ‘شركة تأجير’,
|
||||
perDay: ‘/ يوم’,
|
||||
availableFrom: ‘متاح ابتداءً من’,
|
||||
unavailableNoDate: ‘هذه السيارة غير متاحة مؤقتاً للحجوزات الجديدة.’,
|
||||
},
|
||||
}[language]
|
||||
const vehicle = await marketplaceFetchOrDefault<VehicleDetail | null>(`/marketplace/${params.slug}/vehicles/${params.id}`, null)
|
||||
@@ -90,7 +85,6 @@ export default async function VehicleDetailPage({ params }: { params: { slug: st
|
||||
)
|
||||
}
|
||||
|
||||
const destination = `https://${params.slug}.RentalDriveGo.com/book?vehicleId=${vehicle.id}&ref=marketplace`
|
||||
const availabilityDate = vehicle.nextAvailableAt
|
||||
? new Date(vehicle.nextAvailableAt).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })
|
||||
: null
|
||||
@@ -109,25 +103,33 @@ export default async function VehicleDetailPage({ params }: { params: { slug: st
|
||||
{vehicle.photos.length === 0 && <div className="card p-8 text-sm text-stone-500">{dict.noPhotos}</div>}
|
||||
</div>
|
||||
</section>
|
||||
<aside className="card p-6 h-fit">
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-stone-500">{vehicle.company.brand?.displayName ?? dict.rentalCompany}</p>
|
||||
<h1 className="mt-3 text-4xl font-black text-stone-900">{vehicle.make} {vehicle.model}</h1>
|
||||
<p className="mt-3 text-stone-600">{vehicle.year} · {vehicle.category} · {vehicle.transmission}</p>
|
||||
<p className="mt-6 text-3xl font-black text-stone-900">{formatCurrency(vehicle.dailyRate, 'MAD')}<span className="text-base font-medium text-stone-500"> {dict.perDay}</span></p>
|
||||
{!vehicle.availability && (
|
||||
<div className="mt-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
{availabilityDate ? `${dict.availableFrom} ${availabilityDate}` : dict.unavailableNoDate}
|
||||
<aside className="card p-6 h-fit space-y-6">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-stone-500">{vehicle.company.brand?.displayName ?? dict.rentalCompany}</p>
|
||||
<h1 className="mt-3 text-4xl font-black text-stone-900">{vehicle.make} {vehicle.model}</h1>
|
||||
<p className="mt-3 text-stone-600">{vehicle.year} · {vehicle.category} · {vehicle.transmission}</p>
|
||||
<p className="mt-6 text-3xl font-black text-stone-900">{formatCurrency(vehicle.dailyRate, 'MAD')}<span className="text-base font-medium text-stone-500"> {dict.perDay}</span></p>
|
||||
{!vehicle.availability && (
|
||||
<div className="mt-4 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
{availabilityDate ? `${dict.availableFrom} ${availabilityDate}` : dict.unavailableNoDate}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
{vehicle.features.map((feature) => (
|
||||
<span key={feature} className="rounded-full bg-stone-100 px-3 py-1 text-xs font-medium text-stone-700">{feature}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 flex flex-wrap gap-2">
|
||||
{vehicle.features.map((feature) => (
|
||||
<span key={feature} className="rounded-full bg-stone-100 px-3 py-1 text-xs font-medium text-stone-700">{feature}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<Link href={destination} className="block rounded-full bg-stone-900 px-6 py-3 text-center text-sm font-semibold text-white">{dict.continueTo} {vehicle.company.brand?.displayName ?? dict.rentalCompany} {dict.bookingSite}</Link>
|
||||
<Link href={`/explore/${params.slug}`} className="block rounded-full border border-stone-300 px-6 py-3 text-center text-sm font-semibold text-stone-700">{dict.backToFleet}</Link>
|
||||
</div>
|
||||
|
||||
{vehicle.availability ? (
|
||||
<BookingForm
|
||||
vehicleId={vehicle.id}
|
||||
companySlug={params.slug}
|
||||
dailyRate={vehicle.dailyRate}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Link href={`/explore/${params.slug}`} className="block rounded-full border border-stone-300 px-6 py-3 text-center text-sm font-semibold text-stone-700">{dict.backToFleet}</Link>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -102,7 +102,7 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
|
||||
firstName: 'First name',
|
||||
lastName: 'Last name',
|
||||
email: 'Email address',
|
||||
phone: 'Phone number (optional)',
|
||||
phone: 'Phone number',
|
||||
notes: 'Additional notes (optional)',
|
||||
cancel: 'Cancel',
|
||||
submitRequest: 'Send reservation request',
|
||||
@@ -160,7 +160,7 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
|
||||
firstName: 'Prénom',
|
||||
lastName: 'Nom',
|
||||
email: 'Adresse e-mail',
|
||||
phone: 'Numéro de téléphone (optionnel)',
|
||||
phone: 'Numéro de téléphone',
|
||||
notes: 'Notes supplémentaires (optionnel)',
|
||||
cancel: 'Annuler',
|
||||
submitRequest: 'Envoyer la demande',
|
||||
@@ -218,7 +218,7 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
|
||||
firstName: 'الاسم الأول',
|
||||
lastName: 'اسم العائلة',
|
||||
email: 'البريد الإلكتروني',
|
||||
phone: 'رقم الهاتف (اختياري)',
|
||||
phone: 'رقم الهاتف',
|
||||
notes: 'ملاحظات إضافية (اختياري)',
|
||||
cancel: 'إلغاء',
|
||||
submitRequest: 'إرسال طلب الحجز',
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
'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 get in touch with 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)',
|
||||
},
|
||||
fr: {
|
||||
title: 'Réserver ce véhicule',
|
||||
firstName: 'Prénom',
|
||||
lastName: 'Nom',
|
||||
email: 'Email',
|
||||
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. La société 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)',
|
||||
},
|
||||
ar: {
|
||||
title: 'احجز هذه السيارة',
|
||||
firstName: 'الاسم الأول',
|
||||
lastName: 'اسم العائلة',
|
||||
email: 'البريد الإلكتروني',
|
||||
phone: 'الهاتف',
|
||||
startDate: 'تاريخ الاستلام',
|
||||
endDate: 'تاريخ الإعادة',
|
||||
notes: 'ملاحظات (اختيارية)',
|
||||
submit: 'طلب حجز',
|
||||
submitting: 'جارٍ الإرسال…',
|
||||
successTitle: 'تم استلام الطلب!',
|
||||
successBody: 'تم إرسال طلب الحجز. ستراجعه الشركة وستتواصل معك قريباً.',
|
||||
invalidDates: 'يجب أن يكون تاريخ الإعادة بعد تاريخ الاستلام.',
|
||||
required: 'يرجى تعبئة جميع الحقول المطلوبة.',
|
||||
perDay: '/ يوم',
|
||||
estimatedTotal: 'الإجمالي التقديري',
|
||||
days: 'يوم',
|
||||
},
|
||||
} 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,
|
||||
})
|
||||
setSuccess(true)
|
||||
} catch (err: any) {
|
||||
setError(err.message ?? 'Something went wrong. Please try again.')
|
||||
} 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-stone-700 dark:bg-stone-800 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-stone-700 dark:bg-stone-800 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-stone-700 dark:bg-stone-800 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-stone-700 dark:bg-stone-800 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-stone-700 dark:bg-stone-800 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-stone-700 dark:bg-stone-800 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-stone-700 dark:bg-stone-800 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-stone-700 disabled:opacity-60 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
|
||||
>
|
||||
{submitting ? t.submitting : t.submit}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -189,6 +189,16 @@ function SegmentedControl<T extends string>({
|
||||
)
|
||||
}
|
||||
|
||||
function detectBrowserLanguage(): string | null {
|
||||
if (typeof navigator === 'undefined') return null
|
||||
const langs = Array.from(navigator.languages ?? [navigator.language])
|
||||
for (const lang of langs) {
|
||||
const code = lang.split('-')[0].toLowerCase()
|
||||
if (code === 'fr' || code === 'ar' || code === 'en') return code
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function useMarketplacePreferences() {
|
||||
const context = useContext(PreferencesContext)
|
||||
if (!context) throw new Error('useMarketplacePreferences must be used within MarketplaceShell')
|
||||
@@ -262,6 +272,9 @@ export default function MarketplaceShell({ children }: { children: React.ReactNo
|
||||
|
||||
if (isMarketplaceLanguage(storedLanguage)) {
|
||||
setLanguage(storedLanguage)
|
||||
} else {
|
||||
const detected = detectBrowserLanguage()
|
||||
if (isMarketplaceLanguage(detected)) setLanguage(detected)
|
||||
}
|
||||
|
||||
if (storedTheme === 'light' || storedTheme === 'medium' || storedTheme === 'dark') {
|
||||
|
||||
Reference in New Issue
Block a user