import PlayzerIcon from '@/assets/playzer-favicon-512-transparent.png'; import { Button } from '@/components/ui/button'; import { Field, FieldError, FieldLabel } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; import { Stepper, StepperIndicator, StepperItem, StepperList, StepperSeparator, StepperTitle, StepperTrigger, } from '@/components/ui/stepper'; import { useIsMobile } from '@/hooks/use-mobile'; import { ApiClientError, apiClient } from '@/lib/api-client'; import { useTheme } from '@/lib/theme'; import { zodResolver } from '@hookform/resolvers/zod'; import type { PublicAvailabilityCourt, PublicBookingSport } from '@repo/api-contract'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { ArrowRight, Building2, CalendarDays, Camera, Check, ChevronLeft, ChevronRight, Clock3, Dumbbell, Lock, MapPin, MessageCircle, Moon, Share2, ShieldCheck, Sparkles, Sun, Trophy, Users, Zap, } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; const DAYS_STEP = 5; const DESKTOP_DAYS_STEP = 7; const BUSINESS_START_MINUTES = 8 * 60; const BUSINESS_END_MINUTES = 22 * 60; const bookingFormSchema = z.object({ customerName: z .string() .trim() .min(2, 'Ingresa tu nombre.') .max(120, 'El nombre no puede superar los 120 caracteres.'), customerPhone: z .string() .trim() .min(6, 'Ingresá un teléfono válido.') .max(30, 'El teléfono no puede superar los 30 caracteres.'), customerEmail: z.string().email('Ingresá un email válido.'), }); type BookingFormValues = z.infer; type PublicBookingPageProps = { complexSlug: string; }; type SelectedSlot = { courtId: string; courtName: string; sportId: string; sportName: string; startTime: string; endTime: string; price: number; }; type BookingShellProps = { complexName?: string; complexAddress?: string; sports: PublicBookingSport[]; courts: PublicAvailabilityCourt[]; selectedDate: string; selectedSportId?: string; selectedCourtId?: string; selectedSlot: SelectedSlot | null; dayOptions: DayOption[]; canGoBack: boolean; isLoading: boolean; isError: boolean; sportSelectionRequired?: boolean; errorMessage?: string; autoAdjustedDateNotice: string | null; form: ReturnType>; isCreating: boolean; createError?: string; navigationError: string | null; onSelectDate: (date: string) => void; onPreviousDays: () => void; onNextDays: () => void; onPreviousDay: () => void; onNextDay: () => void; onSelectSport: (sportId: string) => void; onSelectCourt: (courtId: string) => void; onSelectSlot: (slot: SelectedSlot) => void; onSubmit: (values: BookingFormValues) => Promise; }; type DayOption = { value: string; date: Date; weekday: string; dayMonth: string; shortDay: string; monthLabel: string; longLabel: string; }; function addDays(baseDate: Date, amount: number) { const next = new Date(baseDate); next.setDate(next.getDate() + amount); return next; } function toIsoDateLocal(date: Date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function toMinutes(value: string): number { const [hours, minutes] = value.split(':').map((part) => Number(part)); return hours * 60 + minutes; } function toTimeLabel(minutes: number) { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`; } function formatDayLabel(date: Date): DayOption { const weekday = new Intl.DateTimeFormat('es-AR', { weekday: 'short' }) .format(date) .replace('.', ''); const monthLabel = new Intl.DateTimeFormat('es-AR', { month: 'short' }) .format(date) .replace('.', ''); const longLabel = new Intl.DateTimeFormat('es-AR', { weekday: 'long', day: 'numeric', month: 'long', }).format(date); return { value: toIsoDateLocal(date), date, weekday, dayMonth: new Intl.DateTimeFormat('es-AR', { day: '2-digit', month: '2-digit', }).format(date), shortDay: new Intl.DateTimeFormat('es-AR', { day: '2-digit' }).format(date), monthLabel, longLabel: longLabel.charAt(0).toUpperCase() + longLabel.slice(1), }; } function extractMessage(error: unknown, fallback: string) { if (error instanceof ApiClientError) { return error.message || fallback; } return fallback; } function formatBookingPrice(price: number): string { if (price === 0) { return 'Sin cargo'; } return new Intl.NumberFormat('es-AR', { style: 'currency', currency: 'ARS', maximumFractionDigits: Number.isInteger(price) ? 0 : 2, }).format(price); } function getStep(selectedSlot: SelectedSlot | null, isValid: boolean) { if (!selectedSlot) return 1; if (!isValid) return 3; return 4; } function getCompletedSteps(selectedSlot: SelectedSlot | null, isValid: boolean) { return { court: Boolean(selectedSlot), time: Boolean(selectedSlot), details: Boolean(selectedSlot && isValid), confirm: false, }; } function getSelectedCourt( courts: PublicAvailabilityCourt[], selectedCourtId: string | undefined, selectedSlot: SelectedSlot | null ) { if (selectedSlot) { return courts.find((court) => court.courtId === selectedSlot.courtId) ?? courts[0]; } return courts.find((court) => court.courtId === selectedCourtId) ?? courts[0]; } function getTimelineRange(court?: PublicAvailabilityCourt) { if (!court || court.availableSlots.length === 0) { return { start: BUSINESS_START_MINUTES, end: BUSINESS_END_MINUTES, }; } const starts = court.availableSlots.map((slot) => toMinutes(slot.startTime)); const ends = court.availableSlots.map((slot) => toMinutes(slot.endTime)); const firstStart = Math.min(...starts); const lastEnd = Math.max(...ends); return { start: Math.min(BUSINESS_START_MINUTES, Math.floor(firstStart / 60) * 60), end: Math.max(BUSINESS_END_MINUTES, Math.ceil(lastEnd / 60) * 60), }; } function getHourTicks(start: number, end: number, compact = false) { const step = compact ? 120 : 60; const ticks = []; for (let minutes = start; minutes <= end; minutes += step) { ticks.push(minutes); } return ticks; } function isSlotSelected( slot: { startTime: string }, courtId: string, selectedSlot: SelectedSlot | null ) { return selectedSlot?.courtId === courtId && selectedSlot.startTime === slot.startTime; } function getSportIcon(sportName: string) { const lower = sportName.toLowerCase(); if (lower.includes('tenis')) return Zap; if (lower.includes('futbol') || lower.includes('fútbol')) return Dumbbell; if (lower.includes('paddle') || lower.includes('pádel')) return Trophy; if (lower.includes('basquet') || lower.includes('básquet')) return Trophy; if (lower.includes('volei') || lower.includes('vóley')) return Trophy; return Trophy; } function PublicBookingThemeToggle() { const { resolvedTheme, toggleTheme } = useTheme(); const Icon = resolvedTheme === 'dark' ? Sun : Moon; return ( ); } function PlayzerBrand({ compact = false }: { compact?: boolean }) { return (
Playzer Playzer
); } function ProgressSteps({ currentStep, completedSteps, mobile = false, }: { currentStep: number; completedSteps: Record; mobile?: boolean; }) { const steps = [ { value: 'court', label: 'Elegí cancha' }, { value: 'time', label: 'Seleccioná horario' }, { value: 'details', label: 'Tus datos' }, { value: 'confirm', label: 'Confirmación' }, ]; const activeStep = steps[Math.max(0, Math.min(currentStep - 1, steps.length - 1))]?.value; return ( {steps.map((step, index) => { const isCompleted = completedSteps[step.value]; const isActive = step.value === activeStep; return ( {index + 1} {!mobile && ( {step.label} )} ); })} ); } function Legend() { return (
); } function LegendDot({ color, label }: { color: string; label: string }) { return ( {label} ); } function CourtIcon({ className = 'size-5' }: { className?: string }) { return ; } function PublicBookingPageChrome({ children, currentStep, completedSteps, complexName, mobile = false, }: { children: React.ReactNode; currentStep: number; completedSteps: Record; complexName?: string; mobile?: boolean; }) { const { resolvedTheme } = useTheme(); const themeClass = resolvedTheme === 'dark' ? 'public-booking-root public-booking-dark' : 'public-booking-root public-booking-light'; if (mobile) { return (

Reservá tu cancha
de forma rápida y sencilla

Elegí tu cancha, seleccioná el horario y completá tu reserva.

{complexName && (

{complexName}

)}
{children}
); } return (
{complexName && (

{complexName}

)}

Reservá tu cancha
de forma rápida y sencilla

Elegí tu cancha, seleccioná el horario y completá tu reserva.

{children}

© 2026 Playzer. Todos los derechos reservados.

); } function ComplexInfoPanel({ complexName, complexAddress, }: { complexName?: string; complexAddress?: string; }) { return (

{complexName ?? 'Complex'}

{complexAddress && (

{complexAddress}

)}
); } function PublicBookingDesktop(props: BookingShellProps) { const { courts, sports, selectedSportId, selectedCourtId, selectedDate, selectedSlot, dayOptions, canGoBack, isLoading, isError, sportSelectionRequired, errorMessage, autoAdjustedDateNotice, onSelectSport, onSelectCourt, onSelectSlot, } = props; const selectedCourt = getSelectedCourt(courts, selectedCourtId, selectedSlot); const currentStep = getStep(selectedSlot, props.form.formState.isValid); const completedSteps = getCompletedSteps(selectedSlot, props.form.formState.isValid); const selectedDay = dayOptions.find((option) => option.value === selectedDate); const canGoBackButton = canGoBack; return (
{autoAdjustedDateNotice && (

{autoAdjustedDateNotice}

)} {isError && {errorMessage}} {isLoading && Buscando disponibilidad...} {selectedCourt && ( <>
{selectedDay?.longLabel ?? selectedDate}
)}
); } function PublicBookingMobile(props: BookingShellProps) { const selectedCourt = getSelectedCourt(props.courts, props.selectedCourtId, props.selectedSlot); const currentStep = getStep(props.selectedSlot, props.form.formState.isValid); const completedSteps = getCompletedSteps(props.selectedSlot, props.form.formState.isValid); const selectedDay = props.dayOptions.find((option) => option.value === props.selectedDate); return (

Seleccioná tu cancha

{props.sportSelectionRequired && ( ({ value: sport.id, label: sport.name }))} /> )} {props.courts.length > 0 ? ( ) : ( !props.isLoading && (

No hay canchas disponibles para esta fecha.

) )}
Fecha
{selectedDay?.longLabel ?? props.selectedDate}
{props.autoAdjustedDateNotice && (

{props.autoAdjustedDateNotice}

)} {props.isLoading && Buscando disponibilidad...} {props.isError && {props.errorMessage}} {selectedCourt && (
)}
); } function Panel({ children, className = '' }: { children: React.ReactNode; className?: string }) { return (
{children}
); } function StatusPanel({ children }: { children?: React.ReactNode }) { return (

{children}

); } function SportSelector({ label, value, options, onValueChange, }: { label: string; value: string; options: { value: string; label: string }[]; onValueChange: (value: string) => void; }) { return ( {label}
{options.map((option) => { const Icon = getSportIcon(option.label); const isSelected = value === option.value; return ( ); })}
); } function MobileSportSelector({ label, value, options, onValueChange, }: { label: string; value: string; options: { value: string; label: string }[]; onValueChange: (value: string) => void; }) { return (
{label}
{options.map((option) => { const Icon = getSportIcon(option.label); const isSelected = value === option.value; return ( ); })}
); } function MobileCourtPicker({ courts, selectedCourtId, onSelectCourt, }: { courts: PublicAvailabilityCourt[]; selectedCourtId?: string; onSelectCourt: (courtId: string) => void; }) { return (
{courts.map((court) => { const isSelected = selectedCourtId === court.courtId; return ( ); })}
); } function MobileAvailableSlots({ court, selectedSlot, onSelectSlot, }: { court: PublicAvailabilityCourt; selectedSlot: SelectedSlot | null; onSelectSlot: (slot: SelectedSlot) => void; }) { const slots = court.availableSlots; return (

Horarios disponibles

Turnos de {court.slotDurationMinutes} minutos

{slots.length} {slots.length === 1 ? 'turno' : 'turnos'}
{slots.length > 0 ? (
{slots.map((slot) => { const selected = isSlotSelected(slot, court.courtId, selectedSlot); return ( ); })}
) : (

No hay horarios disponibles para esta cancha.

)}
); } function Feature({ icon, title, text }: { icon: React.ReactNode; title: string; text: string }) { return (
{icon}

{title}

{text}

); } function CourtHeader({ court, complexName, compact = false, }: { court?: PublicAvailabilityCourt; complexName?: string; compact?: boolean; }) { return (
{!compact && (
)}

{court?.courtName ?? 'Cancha'}

{court?.sport.name ?? 'Deporte'} {complexName ? ` · ${complexName}` : ''}

); } function BookingTimeline({ court, selectedSlot, onSelectSlot, compact = false, }: { court?: PublicAvailabilityCourt; selectedSlot: SelectedSlot | null; onSelectSlot: (slot: SelectedSlot) => void; compact?: boolean; }) { const range = getTimelineRange(court); const ticks = getHourTicks(range.start, range.end, compact); const total = range.end - range.start; const slots = court?.availableSlots ?? []; return (
{ticks.map((tick) => (
{toTimeLabel(tick)}
))}
{ticks.map((tick) => ( ))} {slots.length > 0 ? ( slots.map((slot) => { const left = ((toMinutes(slot.startTime) - range.start) / total) * 100; const width = ((toMinutes(slot.endTime) - toMinutes(slot.startTime)) / total) * 100; const selected = court ? isSlotSelected(slot, court.courtId, selectedSlot) : false; return ( ); }) ) : (
Sin horarios disponibles
)} {slots.slice(0, compact ? 2 : 3).map((slot, index) => { const end = toMinutes(slot.endTime); const nextStart = slots[index + 1] ? toMinutes(slots[index + 1].startTime) : end + 60; if (nextStart - end < 45 || nextStart > range.end) return null; const left = ((end - range.start) / total) * 100; const width = ((Math.min(nextStart, range.end) - end) / total) * 100; return (
); })} {selectedSlot && court?.courtId === selectedSlot.courtId && (
{selectedSlot.startTime}
)}
); } function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) { const { selectedSlot, form, isCreating, createError, navigationError, onSubmit, compact = false, } = props; const { register, handleSubmit, formState: { errors, isSubmitting, isValid }, } = form; const customerNameInputRef = useRef(null); const customerNameRegistration = register('customerName'); useEffect(() => { if (!selectedSlot) return; window.requestAnimationFrame(() => { customerNameInputRef.current?.focus(); }); }, [selectedSlot]); return (

{selectedSlot ? '¡Hora libre!' : 'Elegí un horario'}

{selectedSlot ? `Este horario está disponible para reservar. ${selectedSlot.startTime} - ${selectedSlot.endTime}` : 'Seleccioná una cancha y un horario disponible.'}

{selectedSlot && (
Precio del turno {formatBookingPrice(selectedSlot.price)}
Nombre { customerNameRegistration.ref(element); customerNameInputRef.current = element; }} /> Telefono
Email

Te enviaremos la confirmación de la reserva por email.

{createError &&

{createError}

} {navigationError &&

{navigationError}

}
)}
); } function MobileBottomNav() { const items = [ { label: 'Reservar', icon: CalendarDays, active: true }, { label: 'Canchas', icon: Building2 }, { label: 'Cómo funciona', icon: Clock3 }, { label: 'Contacto', icon: Users }, ]; return ( ); } export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) { const navigate = useNavigate(); const isMobile = useIsMobile(); const today = useMemo(() => new Date(), []); const todayIsoDate = useMemo(() => toIsoDateLocal(today), [today]); const [windowStartOffset, setWindowStartOffset] = useState(0); const [selectedSportId, setSelectedSportId] = useState(); const [selectedCourtId, setSelectedCourtId] = useState(); const [selectedSlot, setSelectedSlot] = useState(null); const [navigationError, setNavigationError] = useState(null); const [hasAutoAdjustedInitialDate, setHasAutoAdjustedInitialDate] = useState(false); const [autoAdjustedDateNotice, setAutoAdjustedDateNotice] = useState(null); const daysToShow = isMobile ? DAYS_STEP : DESKTOP_DAYS_STEP; const dayOptions = useMemo(() => { return Array.from({ length: daysToShow }).map((_, index) => { const current = addDays(today, windowStartOffset + index); return formatDayLabel(current); }); }, [daysToShow, today, windowStartOffset]); const [selectedDate, setSelectedDate] = useState(dayOptions[0]?.value ?? ''); useEffect(() => { if (dayOptions.some((option) => option.value === selectedDate)) { return; } setSelectedDate(dayOptions[0]?.value ?? ''); }, [dayOptions, selectedDate]); const availabilityQuery = useQuery({ queryKey: ['public-booking-availability', complexSlug, selectedDate, selectedSportId], enabled: Boolean(selectedDate), placeholderData: (previousData) => previousData, queryFn: () => apiClient.publicBookings.getAvailability(complexSlug, { date: selectedDate, ...(selectedSportId ? { sportId: selectedSportId } : {}), }), }); useEffect(() => { const availability = availabilityQuery.data; if (!availability) return; if (!availability.sportSelectionRequired) { if (!selectedSportId && availability.sports[0]) { setSelectedSportId(availability.sports[0].id); } return; } if (selectedSportId && !availability.sports.some((sport) => sport.id === selectedSportId)) { setSelectedSportId(undefined); } }, [availabilityQuery.data, selectedSportId]); const form = useForm({ resolver: zodResolver(bookingFormSchema), mode: 'onChange', defaultValues: { customerName: '', customerPhone: '', customerEmail: '', }, }); const createBookingMutation = useMutation({ mutationFn: (payload: BookingFormValues) => { if (!selectedSlot) { throw new Error('Debés seleccionar un horario.'); } return apiClient.publicBookings.create(complexSlug, { date: selectedDate, sportId: selectedSportId ?? selectedSlot.sportId, courtId: selectedSlot.courtId, startTime: selectedSlot.startTime, customerName: payload.customerName, customerPhone: payload.customerPhone, customerEmail: payload.customerEmail, }); }, }); const onSubmit = async (values: BookingFormValues) => { setNavigationError(null); const booking = await createBookingMutation.mutateAsync(values); if (!booking.bookingCode) { setNavigationError('No se pudo obtener el código de confirmación de la reserva.'); return; } form.reset(); setSelectedSlot(null); await availabilityQuery.refetch(); await navigate({ to: '/$complexSlug/booking/confirmed/$bookingCode', params: { complexSlug, bookingCode: booking.bookingCode, }, }); }; const visibleCourts = useMemo(() => { const courts = availabilityQuery.data?.courts ?? []; if (selectedDate !== todayIsoDate) { return courts; } const now = new Date(); const nowMinutes = now.getHours() * 60 + now.getMinutes(); return courts .map((court) => ({ ...court, availableSlots: court.availableSlots.filter( (slot) => toMinutes(slot.startTime) >= nowMinutes ), })) .filter((court) => court.availableSlots.length > 0); }, [availabilityQuery.data?.courts, selectedDate, todayIsoDate]); useEffect(() => { if (!selectedSlot) return; const stillAvailable = visibleCourts.some( (court) => court.courtId === selectedSlot.courtId && court.availableSlots.some((slot) => slot.startTime === selectedSlot.startTime) ); if (!stillAvailable) { setSelectedSlot(null); } }, [selectedSlot, visibleCourts]); useEffect(() => { if (visibleCourts.length === 0) { setSelectedCourtId(undefined); return; } if (selectedCourtId && visibleCourts.some((court) => court.courtId === selectedCourtId)) { return; } setSelectedCourtId(visibleCourts[0]?.courtId); }, [selectedCourtId, visibleCourts]); useEffect(() => { if (hasAutoAdjustedInitialDate) return; if (selectedDate !== todayIsoDate) { setHasAutoAdjustedInitialDate(true); return; } if (availabilityQuery.isLoading || availabilityQuery.isError || !availabilityQuery.data) { return; } if (visibleCourts.length > 0) { setHasAutoAdjustedInitialDate(true); return; } let cancelled = false; const findNextAvailableDate = async () => { for (let dayOffset = 1; dayOffset <= 30; dayOffset += 1) { const candidateDate = toIsoDateLocal(addDays(today, dayOffset)); try { const availability = await apiClient.publicBookings.getAvailability(complexSlug, { date: candidateDate, ...(selectedSportId ? { sportId: selectedSportId } : {}), }); if (availability.courts.length === 0) { continue; } if (cancelled) return; setWindowStartOffset(dayOffset); setSelectedDate(candidateDate); setSelectedSlot(null); setAutoAdjustedDateNotice( 'No había turnos disponibles para hoy. Te mostramos el próximo día con turnos.' ); setHasAutoAdjustedInitialDate(true); return; } catch { break; } } if (!cancelled) { setHasAutoAdjustedInitialDate(true); } }; void findNextAvailableDate(); return () => { cancelled = true; }; }, [ availabilityQuery.data, availabilityQuery.isError, availabilityQuery.isLoading, complexSlug, hasAutoAdjustedInitialDate, selectedDate, selectedSportId, today, todayIsoDate, visibleCourts.length, ]); const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate); const canGoBack = windowStartOffset > 0 || currentDayIndex > 0; const selectDate = (date: string) => { setSelectedDate(date); setSelectedSlot(null); setAutoAdjustedDateNotice(null); }; const goToPreviousDays = () => { if (!canGoBack) return; setWindowStartOffset((previous) => { const next = Math.max(0, previous - daysToShow); const nextStartDate = toIsoDateLocal(addDays(today, next)); setSelectedDate(nextStartDate); setSelectedSlot(null); setAutoAdjustedDateNotice(null); return next; }); }; const goToNextDays = () => { setWindowStartOffset((previous) => { const next = previous + daysToShow; const nextStartDate = toIsoDateLocal(addDays(today, next)); setSelectedDate(nextStartDate); setSelectedSlot(null); setAutoAdjustedDateNotice(null); return next; }); }; const goToPreviousDay = () => { const currentIndex = dayOptions.findIndex((o) => o.value === selectedDate); if (currentIndex > 0) { const prevDate = dayOptions[currentIndex - 1].value; setSelectedDate(prevDate); setSelectedSlot(null); setAutoAdjustedDateNotice(null); return; } if (windowStartOffset > 0) { const newOffset = windowStartOffset - 1; const newStartDate = toIsoDateLocal(addDays(today, newOffset)); setWindowStartOffset(newOffset); setSelectedDate(newStartDate); setSelectedSlot(null); setAutoAdjustedDateNotice(null); } }; const goToNextDay = () => { const currentIndex = dayOptions.findIndex((o) => o.value === selectedDate); if (currentIndex < dayOptions.length - 1) { const nextDate = dayOptions[currentIndex + 1].value; setSelectedDate(nextDate); setSelectedSlot(null); setAutoAdjustedDateNotice(null); return; } const newOffset = windowStartOffset + 1; const nextDate = toIsoDateLocal(addDays(today, newOffset)); setWindowStartOffset(newOffset); setSelectedDate(nextDate); setSelectedSlot(null); setAutoAdjustedDateNotice(null); }; const shellProps: BookingShellProps = { complexName: availabilityQuery.data?.complexName, complexAddress: availabilityQuery.data?.complexAddress, sports: availabilityQuery.data?.sports ?? [], courts: visibleCourts, selectedDate, selectedSportId, selectedCourtId, selectedSlot, dayOptions, canGoBack, isLoading: availabilityQuery.isLoading, isError: availabilityQuery.isError, sportSelectionRequired: availabilityQuery.data?.sportSelectionRequired, errorMessage: availabilityQuery.isError ? extractMessage(availabilityQuery.error, 'No pudimos cargar la disponibilidad.') : undefined, autoAdjustedDateNotice, form, isCreating: createBookingMutation.isPending, createError: createBookingMutation.isError ? extractMessage(createBookingMutation.error, 'No pudimos confirmar el turno.') : undefined, navigationError, onSelectDate: selectDate, onPreviousDays: goToPreviousDays, onNextDays: goToNextDays, onPreviousDay: goToPreviousDay, onNextDay: goToNextDay, onSelectSport: (sportId) => { setSelectedSportId(sportId); setSelectedCourtId(undefined); setSelectedSlot(null); setAutoAdjustedDateNotice(null); }, onSelectCourt: (courtId) => { setSelectedCourtId(courtId); setSelectedSlot(null); }, onSelectSlot: (slot) => { setSelectedCourtId(slot.courtId); setSelectedSlot(slot); }, onSubmit, }; return isMobile ? ( ) : ( ); }