add text input validation
Build & Push / Pipeline Tests (push) Failing after 1m5s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 52s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped

This commit is contained in:
root
2026-08-31 22:51:44 -04:00
parent b99e8d0be4
commit 057d41e1c2
61 changed files with 968 additions and 302 deletions
@@ -42,13 +42,13 @@ export default function CarplaceSearchForm({ cities, copy, initial = {}, compact
{!compact ? <h2 className="text-xl font-black text-blue-950 dark:text-white">{copy.search.title}</h2> : null}
<div className={fieldsGridClass}>
<SearchField icon={MapPin} label={copy.search.pickup}>
<input list="carplace-cities" name="pickupLocation" defaultValue={initial.pickupLocation ?? ''} placeholder={copy.search.locationPlaceholder} required className="carplace-input" />
<input list="carplace-cities" name="pickupLocation" defaultValue={initial.pickupLocation ?? ''} placeholder={copy.search.locationPlaceholder} required maxLength={255} className="carplace-input" />
</SearchField>
<SearchField icon={MapPin} label={copy.search.returnLocation}>
{dropoffMode === 'same' ? (
<div className="carplace-input flex items-center text-stone-500 dark:text-slate-400">{copy.search.sameReturn}</div>
) : (
<input list="carplace-cities" name="dropoffLocation" defaultValue={initial.dropoffLocation ?? ''} placeholder={copy.search.locationPlaceholder} required className="carplace-input" />
<input list="carplace-cities" name="dropoffLocation" defaultValue={initial.dropoffLocation ?? ''} placeholder={copy.search.locationPlaceholder} required maxLength={255} className="carplace-input" />
)}
</SearchField>
<SearchField icon={CalendarDays} label={copy.search.pickupDate}>
@@ -86,7 +86,7 @@ export default function CarplaceSearchForm({ cities, copy, initial = {}, compact
</label>
<label className="grid gap-1 text-xs font-bold text-stone-600 dark:text-slate-300">
{copy.search.promo} <span className="font-normal text-stone-400">({copy.search.optional})</span>
<input name="promoCode" defaultValue={initial.promoCode ?? ''} className="carplace-input" />
<input name="promoCode" defaultValue={initial.promoCode ?? ''} maxLength={30} pattern="^[A-Z0-9_-]+$" className="carplace-input" />
</label>
</div>
@@ -30,6 +30,13 @@ type FormState = Required<Omit<BookingInitialValues, 'promoCode'>> & {
consent: boolean
}
const MOROCCAN_PHONE_PATTERN = '^(?:(?:\\+|00)212|0)\\s?[5-7](?:\\s?\\d){8}$'
const LOCALIZED_NAME_PATTERNS: Record<CarplaceLanguage, string> = {
en: "^[A-Za-z\\s'-]+$",
fr: "^[A-Za-zÀ-ÖØ-öø-ÿŒœ\\s'-]+$",
ar: "^[\\u0600-\\u06FF\\u0750-\\u077F\\s،؛؟ـ'-]+$",
}
export default function ProgressiveBookingFlow({
vehicleId,
companySlug,
@@ -138,6 +145,12 @@ export default function ProgressiveBookingFlow({
setError(copy.booking.error)
return
}
const namePattern = new RegExp(LOCALIZED_NAME_PATTERNS[language], 'u')
const phonePattern = /^(?:(?:\+|00)212|0)\s?[5-7](?:\s?\d){8}$/
if (!namePattern.test(form.firstName.trim()) || !namePattern.test(form.lastName.trim()) || !phonePattern.test(form.phone.trim())) {
setError(copy.booking.error)
return
}
setStep(3)
void track('contact_details_started')
}
@@ -256,7 +269,7 @@ export default function ProgressiveBookingFlow({
</BookingField>
</div>
<label className="grid gap-1 text-xs font-bold text-stone-600 dark:text-slate-300">{copy.search.age}<select className="carplace-input" value={form.driverAge} onChange={(event) => update('driverAge', event.target.value)}><option value="18">18+</option><option value="21">21+</option><option value="23">23+</option><option value="25">25+</option><option value="30">30+</option></select></label>
<label className="grid gap-1 text-xs font-bold text-stone-600 dark:text-slate-300">{copy.search.promo} <span className="font-normal text-stone-400">({copy.search.optional})</span><input className="carplace-input" value={form.promoCode} onChange={(event) => update('promoCode', event.target.value)} /></label>
<label className="grid gap-1 text-xs font-bold text-stone-600 dark:text-slate-300">{copy.search.promo} <span className="font-normal text-stone-400">({copy.search.optional})</span><input className="carplace-input" value={form.promoCode} maxLength={30} pattern="^[A-Z0-9_-]+$" onChange={(event) => update('promoCode', event.target.value.toUpperCase())} /></label>
{quote ? <QuoteBox quote={quote} language={language} copy={copy} /> : null}
<button type="button" className="carplace-primary-button justify-center" onClick={validateTrip} disabled={loading}>{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ChevronRight className="h-4 w-4 rtl:rotate-180" />}{copy.actions.continue}</button>
</div>
@@ -266,11 +279,11 @@ export default function ProgressiveBookingFlow({
<div className="mt-5 grid gap-4">
<p className="text-sm leading-6 text-stone-600 dark:text-slate-300">{copy.booking.contactIntro}</p>
<div className="grid gap-3 sm:grid-cols-2">
<BookingField label={copy.booking.firstName} icon={UserRound}><input className="carplace-input" value={form.firstName} onChange={(event) => update('firstName', event.target.value)} autoComplete="given-name" /></BookingField>
<BookingField label={copy.booking.lastName} icon={UserRound}><input className="carplace-input" value={form.lastName} onChange={(event) => update('lastName', event.target.value)} autoComplete="family-name" /></BookingField>
<BookingField label={copy.booking.firstName} icon={UserRound}><input className="carplace-input" value={form.firstName} maxLength={50} pattern={LOCALIZED_NAME_PATTERNS[language]} dir={language === 'ar' ? 'rtl' : 'ltr'} onChange={(event) => update('firstName', event.target.value)} autoComplete="given-name" /></BookingField>
<BookingField label={copy.booking.lastName} icon={UserRound}><input className="carplace-input" value={form.lastName} maxLength={50} pattern={LOCALIZED_NAME_PATTERNS[language]} dir={language === 'ar' ? 'rtl' : 'ltr'} onChange={(event) => update('lastName', event.target.value)} autoComplete="family-name" /></BookingField>
</div>
<BookingField label={copy.booking.email} icon={Mail}><input type="email" className="carplace-input" value={form.email} onChange={(event) => update('email', event.target.value)} autoComplete="email" /></BookingField>
<BookingField label={copy.booking.phone} icon={Phone}><input type="tel" className="carplace-input" value={form.phone} onChange={(event) => update('phone', event.target.value)} autoComplete="tel" /></BookingField>
<BookingField label={copy.booking.email} icon={Mail}><input type="email" className="carplace-input" value={form.email} maxLength={254} onChange={(event) => update('email', event.target.value)} autoComplete="email" /></BookingField>
<BookingField label={copy.booking.phone} icon={Phone}><input type="tel" className="carplace-input" value={form.phone} maxLength={20} pattern={MOROCCAN_PHONE_PATTERN} onChange={(event) => update('phone', event.target.value)} autoComplete="tel" /></BookingField>
<label className="grid gap-1 text-xs font-bold text-stone-600 dark:text-slate-300">{copy.booking.notes} <span className="font-normal text-stone-400">({copy.search.optional})</span><textarea className="carplace-input min-h-24 resize-y" value={form.notes} onChange={(event) => update('notes', event.target.value)} placeholder={copy.booking.notesPlaceholder} maxLength={500} /></label>
<div className="grid grid-cols-2 gap-2"><button type="button" className="carplace-secondary-button justify-center" onClick={() => setStep(1)}><ChevronLeft className="h-4 w-4 rtl:rotate-180" />{copy.actions.back}</button><button type="button" className="carplace-primary-button justify-center" onClick={validateContact}><ChevronRight className="h-4 w-4 rtl:rotate-180" />{copy.actions.continue}</button></div>
</div>