Better complex show
This commit is contained in:
@@ -31,7 +31,6 @@ import {
|
||||
CalendarDays,
|
||||
Camera,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
@@ -67,8 +66,8 @@ const bookingFormSchema = z.object({
|
||||
customerPhone: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(6, 'Ingresa un telefono valido.')
|
||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||
.min(6, 'Ingresá un teléfono válido.')
|
||||
.max(30, 'El teléfono no puede superar los 30 caracteres.'),
|
||||
});
|
||||
|
||||
type BookingFormValues = z.infer<typeof bookingFormSchema>;
|
||||
@@ -292,10 +291,10 @@ function ProgressSteps({
|
||||
mobile?: boolean;
|
||||
}) {
|
||||
const steps = [
|
||||
{ value: 'court', label: 'Elegi cancha' },
|
||||
{ value: 'time', label: 'Selecciona horario' },
|
||||
{ value: 'court', label: 'Elegí cancha' },
|
||||
{ value: 'time', label: 'Seleccioná horario' },
|
||||
{ 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;
|
||||
|
||||
@@ -379,11 +378,13 @@ function PublicBookingPageChrome({
|
||||
children,
|
||||
currentStep,
|
||||
completedSteps,
|
||||
complexName,
|
||||
mobile = false,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
currentStep: number;
|
||||
completedSteps: Record<string, boolean>;
|
||||
complexName?: string;
|
||||
mobile?: boolean;
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
@@ -403,13 +404,18 @@ function PublicBookingPageChrome({
|
||||
|
||||
<section className="mt-6 text-center">
|
||||
<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 />
|
||||
de forma rapida y sencilla
|
||||
de forma rápida y sencilla
|
||||
</h1>
|
||||
<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>
|
||||
{complexName && (
|
||||
<p className="mt-3 text-xs font-semibold uppercase tracking-[0.16em] text-emerald-300/80">
|
||||
{complexName}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="mt-5">
|
||||
@@ -430,29 +436,25 @@ function PublicBookingPageChrome({
|
||||
<header className="border-b border-white/10">
|
||||
<div className="mx-auto flex h-20 max-w-[1320px] items-center gap-8 px-10">
|
||||
<PlayzerBrand />
|
||||
<nav className="ml-auto hidden items-center gap-10 text-sm font-medium text-white md:flex">
|
||||
<a href="#como-funciona" className="transition hover:text-emerald-300">
|
||||
Como funciona
|
||||
</a>
|
||||
<a href="#canchas" className="transition hover:text-emerald-300">
|
||||
Canchas
|
||||
</a>
|
||||
<a href="#contacto" className="transition hover:text-emerald-300">
|
||||
Contacto
|
||||
</a>
|
||||
</nav>
|
||||
{complexName && (
|
||||
<div className="ml-auto hidden min-w-0 items-center gap-3 px-4 py-2 md:flex">
|
||||
<div className="min-w-0 text-right">
|
||||
<p className="truncate text-sm font-semibold text-white">{complexName}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<PublicBookingThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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">
|
||||
Reserva <span className="text-emerald-400">tu cancha</span>
|
||||
Reservá <span className="text-emerald-400">tu cancha</span>
|
||||
<br />
|
||||
de forma rapida y sencilla
|
||||
de forma rápida y sencilla
|
||||
</h1>
|
||||
<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>
|
||||
<div className="mt-7">
|
||||
<ProgressSteps currentStep={currentStep} completedSteps={completedSteps} />
|
||||
@@ -511,27 +513,26 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
||||
const selectedDay = dayOptions.find((option) => option.value === selectedDate);
|
||||
|
||||
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">
|
||||
<aside className="space-y-4">
|
||||
<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">
|
||||
{sportSelectionRequired && (
|
||||
<DarkSelect
|
||||
label="Deporte"
|
||||
icon={<Trophy className="size-5" />}
|
||||
value={selectedSportId ?? ''}
|
||||
placeholder="Selecciona"
|
||||
placeholder="Seleccioná"
|
||||
onValueChange={onSelectSport}
|
||||
options={sports.map((sport) => ({ value: sport.id, label: sport.name }))}
|
||||
/>
|
||||
)}
|
||||
<ReadOnlySelect
|
||||
label="Complejo"
|
||||
icon={<Building2 className="size-5" />}
|
||||
value={props.complexName ?? '-'}
|
||||
/>
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-white/76">Canchas disponibles</p>
|
||||
<div id="canchas" className="space-y-2">
|
||||
@@ -575,16 +576,16 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
||||
<Feature
|
||||
icon={<Sparkles className="size-6" />}
|
||||
title="Reserva en pocos pasos"
|
||||
text="Completa tu reserva en menos de 2 minutos"
|
||||
text="Completá tu reserva en menos de 2 minutos"
|
||||
/>
|
||||
<Feature
|
||||
icon={<ShieldCheck className="size-6" />}
|
||||
title="Cancelacion flexible"
|
||||
text="Cancela gratis hasta 2 horas antes"
|
||||
title="Cancelación flexible"
|
||||
text="Cancelá gratis hasta 2 horas antes"
|
||||
/>
|
||||
<Feature
|
||||
icon={<Users className="size-6" />}
|
||||
title="Atencion personalizada"
|
||||
title="Atención personalizada"
|
||||
text="Estamos disponibles para ayudarte"
|
||||
/>
|
||||
</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]"
|
||||
onClick={onPreviousDays}
|
||||
disabled={!canGoBack}
|
||||
aria-label="Mostrar dias anteriores"
|
||||
aria-label="Mostrar días anteriores"
|
||||
>
|
||||
<ChevronLeft className="size-5" />
|
||||
</button>
|
||||
@@ -620,7 +621,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
||||
type="button"
|
||||
className="flex size-11 items-center justify-center border-l border-white/10 transition hover:bg-white/[0.06]"
|
||||
onClick={onNextDays}
|
||||
aria-label="Mostrar proximos dias"
|
||||
aria-label="Mostrar próximos días"
|
||||
>
|
||||
<ChevronRight className="size-5" />
|
||||
</button>
|
||||
@@ -643,14 +644,14 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPreviousDays}
|
||||
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"
|
||||
aria-label="Mostrar dias anteriores"
|
||||
aria-label="Mostrar días anteriores"
|
||||
>
|
||||
<ChevronLeft className="size-5" />
|
||||
</button>
|
||||
@@ -673,7 +674,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
||||
type="button"
|
||||
onClick={onNextDays}
|
||||
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" />
|
||||
</button>
|
||||
@@ -692,25 +693,29 @@ function PublicBookingMobile(props: BookingShellProps) {
|
||||
const selectedDay = props.dayOptions.find((option) => option.value === props.selectedDate);
|
||||
|
||||
return (
|
||||
<PublicBookingPageChrome currentStep={currentStep} completedSteps={completedSteps} mobile>
|
||||
<PublicBookingPageChrome
|
||||
currentStep={currentStep}
|
||||
completedSteps={completedSteps}
|
||||
complexName={props.complexName}
|
||||
mobile
|
||||
>
|
||||
<div className="mt-5 space-y-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">
|
||||
{props.sportSelectionRequired && (
|
||||
<MobileSelect
|
||||
label="Deporte"
|
||||
value={props.selectedSportId ?? ''}
|
||||
placeholder="Selecciona"
|
||||
placeholder="Seleccioná"
|
||||
onValueChange={props.onSelectSport}
|
||||
options={props.sports.map((sport) => ({ value: sport.id, label: sport.name }))}
|
||||
/>
|
||||
)}
|
||||
<MobileReadOnlySelect label="Complejo" value={props.complexName ?? '-'} />
|
||||
<MobileSelect
|
||||
label="Cancha"
|
||||
value={selectedCourt?.courtId ?? ''}
|
||||
placeholder="Selecciona"
|
||||
placeholder="Seleccioná"
|
||||
onValueChange={props.onSelectCourt}
|
||||
options={props.courts.map((court) => ({
|
||||
value: court.courtId,
|
||||
@@ -727,7 +732,7 @@ function PublicBookingMobile(props: BookingShellProps) {
|
||||
onClick={props.onPreviousDays}
|
||||
disabled={!props.canGoBack}
|
||||
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" />
|
||||
</button>
|
||||
@@ -739,7 +744,7 @@ function PublicBookingMobile(props: BookingShellProps) {
|
||||
type="button"
|
||||
onClick={props.onNextDays}
|
||||
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" />
|
||||
</button>
|
||||
@@ -779,7 +784,7 @@ function PublicBookingMobile(props: BookingShellProps) {
|
||||
onClick={props.onPreviousDays}
|
||||
disabled={!props.canGoBack}
|
||||
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" />
|
||||
</button>
|
||||
@@ -802,7 +807,7 @@ function PublicBookingMobile(props: BookingShellProps) {
|
||||
type="button"
|
||||
onClick={props.onNextDays}
|
||||
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" />
|
||||
</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({
|
||||
label,
|
||||
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 }) {
|
||||
return (
|
||||
<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">
|
||||
<p className="flex items-center gap-2">
|
||||
<Users className="size-4 text-emerald-400" />
|
||||
Superficie: Cesped sintetico
|
||||
Superficie: Césped sintético
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Dumbbell className="size-4 text-emerald-400" />
|
||||
@@ -1112,7 +1088,7 @@ function CourtDetails({ court }: { court?: PublicAvailabilityCourt }) {
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Sparkles className="size-4 text-emerald-400" />
|
||||
Iluminacion LED
|
||||
Iluminación LED
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Shirt className="size-4 text-emerald-400" />
|
||||
@@ -1158,11 +1134,13 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
||||
<Check className="size-7" />
|
||||
</div>
|
||||
<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">
|
||||
{selectedSlot
|
||||
? `Este horario esta disponible para reservar. ${selectedSlot.startTime} - ${selectedSlot.endTime}`
|
||||
: 'Selecciona una cancha y un horario disponible.'}
|
||||
? `Este horario está disponible para reservar. ${selectedSlot.startTime} - ${selectedSlot.endTime}`
|
||||
: 'Seleccioná una cancha y un horario disponible.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1228,7 +1206,7 @@ function MobileBottomNav() {
|
||||
const items = [
|
||||
{ label: 'Reservar', icon: CalendarDays, active: true },
|
||||
{ label: 'Canchas', icon: Building2 },
|
||||
{ label: 'Como funciona', icon: Clock3 },
|
||||
{ label: 'Cómo funciona', icon: Clock3 },
|
||||
{ label: 'Contacto', icon: Users },
|
||||
];
|
||||
|
||||
@@ -1327,7 +1305,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
||||
const createBookingMutation = useMutation({
|
||||
mutationFn: (payload: BookingFormValues) => {
|
||||
if (!selectedSlot) {
|
||||
throw new Error('Debes seleccionar un horario.');
|
||||
throw new Error('Debés seleccionar un horario.');
|
||||
}
|
||||
|
||||
return apiClient.publicBookings.create(complexSlug, {
|
||||
@@ -1346,7 +1324,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
||||
const booking = await createBookingMutation.mutateAsync(values);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1446,7 +1424,7 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
||||
setSelectedDate(candidateDate);
|
||||
setSelectedSlot(null);
|
||||
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);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user