Better complex show

This commit is contained in:
Jose Selesan
2026-05-19 09:42:31 -03:00
parent 900e6146e7
commit 0b6041d146

View File

@@ -31,7 +31,6 @@ import {
CalendarDays, CalendarDays,
Camera, Camera,
Check, Check,
ChevronDown,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Clock3, Clock3,
@@ -67,8 +66,8 @@ const bookingFormSchema = z.object({
customerPhone: z customerPhone: z
.string() .string()
.trim() .trim()
.min(6, 'Ingresa un telefono valido.') .min(6, 'Ingresá un teléfono válido.')
.max(30, 'El telefono no puede superar los 30 caracteres.'), .max(30, 'El teléfono no puede superar los 30 caracteres.'),
}); });
type BookingFormValues = z.infer<typeof bookingFormSchema>; type BookingFormValues = z.infer<typeof bookingFormSchema>;
@@ -292,10 +291,10 @@ function ProgressSteps({
mobile?: boolean; mobile?: boolean;
}) { }) {
const steps = [ const steps = [
{ value: 'court', label: 'Elegi cancha' }, { value: 'court', label: 'Elegí cancha' },
{ value: 'time', label: 'Selecciona horario' }, { value: 'time', label: 'Seleccioná horario' },
{ value: 'details', label: 'Tus datos' }, { value: 'details', label: 'Tus datos' },
{ value: 'confirm', label: 'Confirmacion' }, { value: 'confirm', label: 'Confirmación' },
]; ];
const activeStep = steps[Math.max(0, Math.min(currentStep - 1, steps.length - 1))]?.value; const activeStep = steps[Math.max(0, Math.min(currentStep - 1, steps.length - 1))]?.value;
@@ -379,11 +378,13 @@ function PublicBookingPageChrome({
children, children,
currentStep, currentStep,
completedSteps, completedSteps,
complexName,
mobile = false, mobile = false,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
currentStep: number; currentStep: number;
completedSteps: Record<string, boolean>; completedSteps: Record<string, boolean>;
complexName?: string;
mobile?: boolean; mobile?: boolean;
}) { }) {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
@@ -403,13 +404,18 @@ function PublicBookingPageChrome({
<section className="mt-6 text-center"> <section className="mt-6 text-center">
<h1 className="text-balance text-2xl font-bold leading-tight tracking-normal"> <h1 className="text-balance text-2xl font-bold leading-tight tracking-normal">
Reserva <span className="text-emerald-400">tu cancha</span> Reservá <span className="text-emerald-400">tu cancha</span>
<br /> <br />
de forma rapida y sencilla de forma rápida y sencilla
</h1> </h1>
<p className="mx-auto mt-2 max-w-[260px] text-sm leading-5 text-white/66"> <p className="mx-auto mt-2 max-w-[260px] text-sm leading-5 text-white/66">
Elegi tu cancha, selecciona el horario y completa tu reserva. Elegí tu cancha, seleccioná el horario y completá tu reserva.
</p> </p>
{complexName && (
<p className="mt-3 text-xs font-semibold uppercase tracking-[0.16em] text-emerald-300/80">
{complexName}
</p>
)}
</section> </section>
<div className="mt-5"> <div className="mt-5">
@@ -430,29 +436,25 @@ function PublicBookingPageChrome({
<header className="border-b border-white/10"> <header className="border-b border-white/10">
<div className="mx-auto flex h-20 max-w-[1320px] items-center gap-8 px-10"> <div className="mx-auto flex h-20 max-w-[1320px] items-center gap-8 px-10">
<PlayzerBrand /> <PlayzerBrand />
<nav className="ml-auto hidden items-center gap-10 text-sm font-medium text-white md:flex"> {complexName && (
<a href="#como-funciona" className="transition hover:text-emerald-300"> <div className="ml-auto hidden min-w-0 items-center gap-3 px-4 py-2 md:flex">
Como funciona <div className="min-w-0 text-right">
</a> <p className="truncate text-sm font-semibold text-white">{complexName}</p>
<a href="#canchas" className="transition hover:text-emerald-300"> </div>
Canchas </div>
</a> )}
<a href="#contacto" className="transition hover:text-emerald-300">
Contacto
</a>
</nav>
<PublicBookingThemeToggle /> <PublicBookingThemeToggle />
</div> </div>
</header> </header>
<section className="mx-auto max-w-[1320px] px-10 py-8"> <section className="mx-auto max-w-[1320px] px-10 py-8">
<h1 className="max-w-[560px] text-balance text-4xl font-bold leading-tight tracking-normal"> <h1 className="max-w-[560px] text-balance text-4xl font-bold leading-tight tracking-normal">
Reserva <span className="text-emerald-400">tu cancha</span> Reservá <span className="text-emerald-400">tu cancha</span>
<br /> <br />
de forma rapida y sencilla de forma rápida y sencilla
</h1> </h1>
<p className="mt-3 text-sm text-white/68"> <p className="mt-3 text-sm text-white/68">
Elegi tu cancha, selecciona el horario y completa tu reserva. Elegí tu cancha, seleccioná el horario y completá tu reserva.
</p> </p>
<div className="mt-7"> <div className="mt-7">
<ProgressSteps currentStep={currentStep} completedSteps={completedSteps} /> <ProgressSteps currentStep={currentStep} completedSteps={completedSteps} />
@@ -511,27 +513,26 @@ function PublicBookingDesktop(props: BookingShellProps) {
const selectedDay = dayOptions.find((option) => option.value === selectedDate); const selectedDay = dayOptions.find((option) => option.value === selectedDate);
return ( return (
<PublicBookingPageChrome currentStep={currentStep} completedSteps={completedSteps}> <PublicBookingPageChrome
currentStep={currentStep}
completedSteps={completedSteps}
complexName={props.complexName}
>
<div className="mx-auto grid max-w-[1320px] grid-cols-[330px_minmax(0,1fr)] gap-5 px-10"> <div className="mx-auto grid max-w-[1320px] grid-cols-[330px_minmax(0,1fr)] gap-5 px-10">
<aside className="space-y-4"> <aside className="space-y-4">
<Panel className="p-5"> <Panel className="p-5">
<h2 className="text-lg font-semibold">Selecciona tu cancha</h2> <h2 className="text-lg font-semibold">Seleccioná tu cancha</h2>
<div className="mt-5 space-y-4"> <div className="mt-5 space-y-4">
{sportSelectionRequired && ( {sportSelectionRequired && (
<DarkSelect <DarkSelect
label="Deporte" label="Deporte"
icon={<Trophy className="size-5" />} icon={<Trophy className="size-5" />}
value={selectedSportId ?? ''} value={selectedSportId ?? ''}
placeholder="Selecciona" placeholder="Seleccioná"
onValueChange={onSelectSport} onValueChange={onSelectSport}
options={sports.map((sport) => ({ value: sport.id, label: sport.name }))} options={sports.map((sport) => ({ value: sport.id, label: sport.name }))}
/> />
)} )}
<ReadOnlySelect
label="Complejo"
icon={<Building2 className="size-5" />}
value={props.complexName ?? '-'}
/>
<div> <div>
<p className="mb-2 text-sm text-white/76">Canchas disponibles</p> <p className="mb-2 text-sm text-white/76">Canchas disponibles</p>
<div id="canchas" className="space-y-2"> <div id="canchas" className="space-y-2">
@@ -575,16 +576,16 @@ function PublicBookingDesktop(props: BookingShellProps) {
<Feature <Feature
icon={<Sparkles className="size-6" />} icon={<Sparkles className="size-6" />}
title="Reserva en pocos pasos" title="Reserva en pocos pasos"
text="Completa tu reserva en menos de 2 minutos" text="Completá tu reserva en menos de 2 minutos"
/> />
<Feature <Feature
icon={<ShieldCheck className="size-6" />} icon={<ShieldCheck className="size-6" />}
title="Cancelacion flexible" title="Cancelación flexible"
text="Cancela gratis hasta 2 horas antes" text="Cancelá gratis hasta 2 horas antes"
/> />
<Feature <Feature
icon={<Users className="size-6" />} icon={<Users className="size-6" />}
title="Atencion personalizada" title="Atención personalizada"
text="Estamos disponibles para ayudarte" text="Estamos disponibles para ayudarte"
/> />
</div> </div>
@@ -608,7 +609,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
className="flex size-11 items-center justify-center border-r border-white/10 transition hover:bg-white/[0.06]" className="flex size-11 items-center justify-center border-r border-white/10 transition hover:bg-white/[0.06]"
onClick={onPreviousDays} onClick={onPreviousDays}
disabled={!canGoBack} disabled={!canGoBack}
aria-label="Mostrar dias anteriores" aria-label="Mostrar días anteriores"
> >
<ChevronLeft className="size-5" /> <ChevronLeft className="size-5" />
</button> </button>
@@ -620,7 +621,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
type="button" type="button"
className="flex size-11 items-center justify-center border-l border-white/10 transition hover:bg-white/[0.06]" className="flex size-11 items-center justify-center border-l border-white/10 transition hover:bg-white/[0.06]"
onClick={onNextDays} onClick={onNextDays}
aria-label="Mostrar proximos dias" aria-label="Mostrar próximos días"
> >
<ChevronRight className="size-5" /> <ChevronRight className="size-5" />
</button> </button>
@@ -643,14 +644,14 @@ function PublicBookingDesktop(props: BookingShellProps) {
</div> </div>
<Panel className="p-5"> <Panel className="p-5">
<h3 className="font-semibold">Cambia de dia</h3> <h3 className="font-semibold">Cambiá de día</h3>
<div className="mt-4 grid grid-cols-[36px_repeat(7,minmax(0,1fr))_36px] items-stretch gap-3"> <div className="mt-4 grid grid-cols-[36px_repeat(7,minmax(0,1fr))_36px] items-stretch gap-3">
<button <button
type="button" type="button"
onClick={onPreviousDays} onClick={onPreviousDays}
disabled={!canGoBack} disabled={!canGoBack}
className="flex items-center justify-center rounded-md bg-white/[0.04] text-white transition hover:bg-white/[0.08] disabled:opacity-35" className="flex items-center justify-center rounded-md bg-white/[0.04] text-white transition hover:bg-white/[0.08] disabled:opacity-35"
aria-label="Mostrar dias anteriores" aria-label="Mostrar días anteriores"
> >
<ChevronLeft className="size-5" /> <ChevronLeft className="size-5" />
</button> </button>
@@ -673,7 +674,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
type="button" type="button"
onClick={onNextDays} onClick={onNextDays}
className="flex items-center justify-center rounded-md bg-white/[0.04] text-white transition hover:bg-white/[0.08]" className="flex items-center justify-center rounded-md bg-white/[0.04] text-white transition hover:bg-white/[0.08]"
aria-label="Mostrar proximos dias" aria-label="Mostrar próximos días"
> >
<ChevronRight className="size-5" /> <ChevronRight className="size-5" />
</button> </button>
@@ -692,25 +693,29 @@ function PublicBookingMobile(props: BookingShellProps) {
const selectedDay = props.dayOptions.find((option) => option.value === props.selectedDate); const selectedDay = props.dayOptions.find((option) => option.value === props.selectedDate);
return ( return (
<PublicBookingPageChrome currentStep={currentStep} completedSteps={completedSteps} mobile> <PublicBookingPageChrome
currentStep={currentStep}
completedSteps={completedSteps}
complexName={props.complexName}
mobile
>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
<Panel className="p-3"> <Panel className="p-3">
<h2 className="text-base font-semibold">Selecciona tu cancha</h2> <h2 className="text-base font-semibold">Seleccioná tu cancha</h2>
<div className="mt-3 overflow-hidden rounded-md border border-white/10"> <div className="mt-3 overflow-hidden rounded-md border border-white/10">
{props.sportSelectionRequired && ( {props.sportSelectionRequired && (
<MobileSelect <MobileSelect
label="Deporte" label="Deporte"
value={props.selectedSportId ?? ''} value={props.selectedSportId ?? ''}
placeholder="Selecciona" placeholder="Seleccioná"
onValueChange={props.onSelectSport} onValueChange={props.onSelectSport}
options={props.sports.map((sport) => ({ value: sport.id, label: sport.name }))} options={props.sports.map((sport) => ({ value: sport.id, label: sport.name }))}
/> />
)} )}
<MobileReadOnlySelect label="Complejo" value={props.complexName ?? '-'} />
<MobileSelect <MobileSelect
label="Cancha" label="Cancha"
value={selectedCourt?.courtId ?? ''} value={selectedCourt?.courtId ?? ''}
placeholder="Selecciona" placeholder="Seleccioná"
onValueChange={props.onSelectCourt} onValueChange={props.onSelectCourt}
options={props.courts.map((court) => ({ options={props.courts.map((court) => ({
value: court.courtId, value: court.courtId,
@@ -727,7 +732,7 @@ function PublicBookingMobile(props: BookingShellProps) {
onClick={props.onPreviousDays} onClick={props.onPreviousDays}
disabled={!props.canGoBack} disabled={!props.canGoBack}
className="flex size-9 items-center justify-center rounded-md border border-white/10 bg-white/[0.035] disabled:opacity-35" className="flex size-9 items-center justify-center rounded-md border border-white/10 bg-white/[0.035] disabled:opacity-35"
aria-label="Mostrar dias anteriores" aria-label="Mostrar días anteriores"
> >
<ChevronLeft className="size-4" /> <ChevronLeft className="size-4" />
</button> </button>
@@ -739,7 +744,7 @@ function PublicBookingMobile(props: BookingShellProps) {
type="button" type="button"
onClick={props.onNextDays} onClick={props.onNextDays}
className="flex size-9 items-center justify-center rounded-md border border-white/10 bg-white/[0.035]" className="flex size-9 items-center justify-center rounded-md border border-white/10 bg-white/[0.035]"
aria-label="Mostrar proximos dias" aria-label="Mostrar próximos días"
> >
<ChevronRight className="size-4" /> <ChevronRight className="size-4" />
</button> </button>
@@ -779,7 +784,7 @@ function PublicBookingMobile(props: BookingShellProps) {
onClick={props.onPreviousDays} onClick={props.onPreviousDays}
disabled={!props.canGoBack} disabled={!props.canGoBack}
className="flex items-center justify-center rounded-md bg-white/[0.04] disabled:opacity-35" className="flex items-center justify-center rounded-md bg-white/[0.04] disabled:opacity-35"
aria-label="Mostrar dias anteriores" aria-label="Mostrar días anteriores"
> >
<ChevronLeft className="size-4" /> <ChevronLeft className="size-4" />
</button> </button>
@@ -802,7 +807,7 @@ function PublicBookingMobile(props: BookingShellProps) {
type="button" type="button"
onClick={props.onNextDays} onClick={props.onNextDays}
className="flex items-center justify-center rounded-md bg-white/[0.04]" className="flex items-center justify-center rounded-md bg-white/[0.04]"
aria-label="Mostrar proximos dias" aria-label="Mostrar próximos días"
> >
<ChevronRight className="size-4" /> <ChevronRight className="size-4" />
</button> </button>
@@ -866,23 +871,6 @@ function DarkSelect({
); );
} }
function ReadOnlySelect({
label,
icon,
value,
}: { label: string; icon: React.ReactNode; value: string }) {
return (
<div>
<p className="mb-2 text-sm text-white/76">{label}</p>
<div className="flex h-10 items-center gap-3 rounded-md border border-white/10 bg-white/[0.035] px-3 text-sm text-white">
{icon}
<span className="min-w-0 flex-1 truncate">{value}</span>
<ChevronDown className="size-4 text-white/70" />
</div>
</div>
);
}
function MobileSelect({ function MobileSelect({
label, label,
value, value,
@@ -915,18 +903,6 @@ function MobileSelect({
); );
} }
function MobileReadOnlySelect({ label, value }: { label: string; value: string }) {
return (
<div className="grid grid-cols-[88px_minmax(0,1fr)] items-center border-b border-white/10 bg-white/[0.025] px-3 py-2 last:border-b-0">
<span className="text-xs text-white/66">{label}</span>
<span className="flex min-w-0 items-center justify-between gap-2 text-xs text-white">
<span className="truncate">{value}</span>
<ChevronDown className="size-4 shrink-0 text-white/66" />
</span>
</div>
);
}
function Feature({ icon, title, text }: { icon: React.ReactNode; title: string; text: string }) { function Feature({ icon, title, text }: { icon: React.ReactNode; title: string; text: string }) {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
@@ -1104,7 +1080,7 @@ function CourtDetails({ court }: { court?: PublicAvailabilityCourt }) {
<div className="mt-4 space-y-2 text-sm text-white/62"> <div className="mt-4 space-y-2 text-sm text-white/62">
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<Users className="size-4 text-emerald-400" /> <Users className="size-4 text-emerald-400" />
Superficie: Cesped sintetico Superficie: Césped sintético
</p> </p>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<Dumbbell className="size-4 text-emerald-400" /> <Dumbbell className="size-4 text-emerald-400" />
@@ -1112,7 +1088,7 @@ function CourtDetails({ court }: { court?: PublicAvailabilityCourt }) {
</p> </p>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<Sparkles className="size-4 text-emerald-400" /> <Sparkles className="size-4 text-emerald-400" />
Iluminacion LED Iluminación LED
</p> </p>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<Shirt className="size-4 text-emerald-400" /> <Shirt className="size-4 text-emerald-400" />
@@ -1158,11 +1134,13 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
<Check className="size-7" /> <Check className="size-7" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-lg font-bold">{selectedSlot ? 'Hora libre!' : 'Elegi un horario'}</h3> <h3 className="text-lg font-bold">
{selectedSlot ? '¡Hora libre!' : 'Elegí un horario'}
</h3>
<p className="mt-1 text-sm text-white/66"> <p className="mt-1 text-sm text-white/66">
{selectedSlot {selectedSlot
? `Este horario esta disponible para reservar. ${selectedSlot.startTime} - ${selectedSlot.endTime}` ? `Este horario está disponible para reservar. ${selectedSlot.startTime} - ${selectedSlot.endTime}`
: 'Selecciona una cancha y un horario disponible.'} : 'Seleccioná una cancha y un horario disponible.'}
</p> </p>
</div> </div>
</div> </div>
@@ -1228,7 +1206,7 @@ function MobileBottomNav() {
const items = [ const items = [
{ label: 'Reservar', icon: CalendarDays, active: true }, { label: 'Reservar', icon: CalendarDays, active: true },
{ label: 'Canchas', icon: Building2 }, { label: 'Canchas', icon: Building2 },
{ label: 'Como funciona', icon: Clock3 }, { label: 'Cómo funciona', icon: Clock3 },
{ label: 'Contacto', icon: Users }, { label: 'Contacto', icon: Users },
]; ];
@@ -1327,7 +1305,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
const createBookingMutation = useMutation({ const createBookingMutation = useMutation({
mutationFn: (payload: BookingFormValues) => { mutationFn: (payload: BookingFormValues) => {
if (!selectedSlot) { if (!selectedSlot) {
throw new Error('Debes seleccionar un horario.'); throw new Error('Debés seleccionar un horario.');
} }
return apiClient.publicBookings.create(complexSlug, { return apiClient.publicBookings.create(complexSlug, {
@@ -1346,7 +1324,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
const booking = await createBookingMutation.mutateAsync(values); const booking = await createBookingMutation.mutateAsync(values);
if (!booking.bookingCode) { if (!booking.bookingCode) {
setNavigationError('No se pudo obtener el codigo de confirmacion de la reserva.'); setNavigationError('No se pudo obtener el código de confirmación de la reserva.');
return; return;
} }
@@ -1446,7 +1424,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
setSelectedDate(candidateDate); setSelectedDate(candidateDate);
setSelectedSlot(null); setSelectedSlot(null);
setAutoAdjustedDateNotice( setAutoAdjustedDateNotice(
'No habia turnos disponibles para hoy. Te mostramos el proximo dia con turnos.' 'No había turnos disponibles para hoy. Te mostramos el próximo día con turnos.'
); );
setHasAutoAdjustedInitialDate(true); setHasAutoAdjustedInitialDate(true);
return; return;