Compare commits
9 Commits
feat/court
...
c278c78e3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c278c78e3d | ||
|
|
d767ac3ac7 | ||
|
|
8c3ca0e7e1 | ||
|
|
72f273354e | ||
|
|
ef2ded9d64 | ||
|
|
4c7b825129 | ||
| 339e6a70d7 | |||
|
|
c70541b4f5 | ||
| 89673c3844 |
@@ -770,7 +770,7 @@ export async function cancelPublicBooking(complexSlug: string, input: CancelPubl
|
||||
}
|
||||
|
||||
if (booking.customerPhone !== input.customerPhone.trim()) {
|
||||
throw new PublicBookingServiceError('Los datos ingresados no coinciden con la reserva.', 403);
|
||||
throw new PublicBookingServiceError('Los datos ingresados no coinciden con la reserva.', 400);
|
||||
}
|
||||
|
||||
const date = formatIsoDate(booking.bookingDate);
|
||||
|
||||
@@ -147,6 +147,9 @@ function buildSegmentsForCourt(
|
||||
const segments: BookingTimelineSegment[] = [];
|
||||
const slotDuration = court.slotDurationMinutes;
|
||||
|
||||
const nowMinutes = timeToMinutes(getNowTime());
|
||||
const isToday = isTodayIso(selectedDate);
|
||||
|
||||
const availabilityRanges = court.availability
|
||||
.filter((range) => range.dayOfWeek === dayOfWeek)
|
||||
.map((range) => ({
|
||||
@@ -181,6 +184,9 @@ function buildSegmentsForCourt(
|
||||
slotStart += slotDuration
|
||||
) {
|
||||
const slotEnd = slotStart + slotDuration;
|
||||
|
||||
if (isToday && slotEnd <= nowMinutes) continue;
|
||||
|
||||
const isBooked = courtBookings.some((booking) =>
|
||||
overlapsBooking(slotStart, slotEnd, booking)
|
||||
);
|
||||
|
||||
@@ -3,10 +3,11 @@ import { authClient } from '@/lib/api-client';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import type { ComplexWithRole } from '@repo/api-contract';
|
||||
import { useState } from 'react';
|
||||
import { BookingProvider } from './booking-provider';
|
||||
import { BookingProvider, useBooking } from './booking-provider';
|
||||
import { BookingCreateDialog } from './components/booking-create-dialog';
|
||||
import { BookingDaySummary, BookingQuickActions } from './components/booking-day-summary';
|
||||
import { BookingHeader } from './components/booking-header';
|
||||
import { BookingListView } from './components/booking-list-view';
|
||||
import { BookingMobile } from './components/booking-mobile';
|
||||
import { BookingTimeline } from './components/booking-timeline';
|
||||
import { BookingToolbar } from './components/booking-toolbar';
|
||||
@@ -17,7 +18,6 @@ interface BookingProps {
|
||||
}
|
||||
|
||||
export function Booking({ complex }: BookingProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const { user } = useAuth();
|
||||
const [sending, setSending] = useState(false);
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
@@ -56,21 +56,34 @@ export function Booking({ complex }: BookingProps) {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<BookingMobile />
|
||||
) : (
|
||||
<div className="flex flex-col gap-5">
|
||||
<BookingHeader />
|
||||
<BookingToolbar />
|
||||
<BookingTimeline />
|
||||
<div className="grid gap-5 xl:grid-cols-4">
|
||||
<BookingDaySummary />
|
||||
<BookingQuickActions />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<BookingInner />
|
||||
<BookingCreateDialog />
|
||||
<BookingToolsDialog />
|
||||
</BookingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function BookingInner() {
|
||||
const { viewMode } = useBooking();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return <BookingListView />;
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return <BookingMobile />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<BookingHeader />
|
||||
<BookingToolbar />
|
||||
<BookingTimeline />
|
||||
<div className="grid gap-5 xl:grid-cols-4">
|
||||
<BookingDaySummary />
|
||||
<BookingQuickActions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminBooking, Court } from '@repo/api-contract';
|
||||
|
||||
export type BookingViewMode = 'panel' | 'status';
|
||||
export type BookingViewMode = 'panel' | 'status' | 'list';
|
||||
export type BookingStatusFilter = 'all' | 'free' | 'reserved';
|
||||
export type BookingSegmentStatus = 'free' | 'reserved';
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import { useBooking } from '../booking-provider';
|
||||
import {
|
||||
fromIsoDateLocal,
|
||||
getDayOfWeek,
|
||||
getNowTime,
|
||||
isTodayIso,
|
||||
minutesToTime,
|
||||
timeToMinutes,
|
||||
toIsoDateLocal,
|
||||
@@ -128,6 +130,9 @@ export function BookingCreateDialog() {
|
||||
minute += selectedCourt.slotDurationMinutes
|
||||
) {
|
||||
const slotEnd = minute + selectedCourt.slotDurationMinutes;
|
||||
|
||||
if (isTodayIso(date) && minute <= timeToMinutes(getNowTime())) continue;
|
||||
|
||||
const isBooked = courtBookings.some((booking) => {
|
||||
const bookingStart = timeToMinutes(booking.startTime);
|
||||
const bookingEnd = timeToMinutes(booking.endTime);
|
||||
|
||||
@@ -66,10 +66,8 @@ function SummaryCard({ label, value, detail, icon: Icon, className }: SummaryCar
|
||||
}
|
||||
|
||||
export function BookingQuickActions() {
|
||||
const { selectedDate, selectedStatus, setSelectedDate, setSelectedStatus, exportDayReport } =
|
||||
useBooking();
|
||||
const { setSelectedDate, setViewMode, exportDayReport } = useBooking();
|
||||
const todayIso = toIsoDateLocal(new Date());
|
||||
const isViewingTodayReservations = selectedDate === todayIso && selectedStatus === 'reserved';
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border bg-card/85 p-5 shadow-sm">
|
||||
@@ -79,13 +77,12 @@ export function BookingQuickActions() {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedDate(todayIso);
|
||||
setSelectedStatus(isViewingTodayReservations ? 'all' : 'reserved');
|
||||
setViewMode('list');
|
||||
}}
|
||||
aria-pressed={isViewingTodayReservations}
|
||||
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted aria-pressed:border-reserved/50 aria-pressed:bg-reserved/10"
|
||||
className="flex h-10 items-center gap-3 rounded-md border bg-background/45 px-3 text-left text-sm transition-colors hover:bg-muted"
|
||||
>
|
||||
<CalendarDays className="size-4 text-muted-foreground" />
|
||||
{isViewingTodayReservations ? 'Ver todos los estados' : 'Ver reservas de hoy'}
|
||||
Ver reservas de hoy
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarDays,
|
||||
Clock,
|
||||
MapPin,
|
||||
Phone,
|
||||
ShieldCheck,
|
||||
User,
|
||||
UserX,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useBooking } from '../booking-provider';
|
||||
import type { BookingTimelineSegment } from '../booking.types';
|
||||
import { formatBookingDate } from '../lib/booking-time';
|
||||
|
||||
const statusConfig: Record<string, { label: string; className: string }> = {
|
||||
CONFIRMED: {
|
||||
label: 'Reservada',
|
||||
className: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30',
|
||||
},
|
||||
COMPLETED: {
|
||||
label: 'Completada',
|
||||
className: 'bg-sky-500/15 text-sky-600 dark:text-sky-400 border-sky-500/30',
|
||||
},
|
||||
CANCELLED: {
|
||||
label: 'Cancelada',
|
||||
className: 'bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/30',
|
||||
},
|
||||
NOSHOW: {
|
||||
label: 'No show',
|
||||
className: 'bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/30',
|
||||
},
|
||||
};
|
||||
|
||||
const statusIconConfig: Record<string, typeof ShieldCheck> = {
|
||||
CONFIRMED: ShieldCheck,
|
||||
COMPLETED: ShieldCheck,
|
||||
CANCELLED: UserX,
|
||||
NOSHOW: UserX,
|
||||
};
|
||||
|
||||
export function BookingListView() {
|
||||
const {
|
||||
selectedDate,
|
||||
setViewMode,
|
||||
schedules,
|
||||
openBookingTools,
|
||||
isLoading,
|
||||
isError,
|
||||
errorMessage,
|
||||
} = useBooking();
|
||||
|
||||
const reservedSegments = useMemo(() => {
|
||||
return schedules
|
||||
.flatMap((schedule) =>
|
||||
schedule.segments
|
||||
.filter((segment) => segment.booking)
|
||||
.map((segment) => ({ schedule, segment }))
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const timeDiff = a.segment.startMinutes - b.segment.startMinutes;
|
||||
if (timeDiff !== 0) return timeDiff;
|
||||
return a.schedule.court.name.localeCompare(b.schedule.court.name);
|
||||
});
|
||||
}, [schedules]);
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border bg-card/85 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-b px-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-full"
|
||||
onClick={() => setViewMode('panel')}
|
||||
>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">Reservas del día</h2>
|
||||
<p className="text-sm capitalize text-muted-foreground">
|
||||
{formatBookingDate(selectedDate, { year: 'numeric' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CalendarDays className="size-4" />
|
||||
<span>
|
||||
{reservedSegments.length} {reservedSegments.length === 1 ? 'reserva' : 'reservas'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="px-4 py-10 text-sm text-muted-foreground">Cargando reservas...</div>
|
||||
)}
|
||||
|
||||
{isError && !isLoading && (
|
||||
<div className="px-4 py-10 text-sm text-destructive">{errorMessage}</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && reservedSegments.length === 0 && (
|
||||
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
<CalendarDays className="mx-auto mb-3 size-10 opacity-40" />
|
||||
<p>No hay reservas para este día.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && reservedSegments.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
{/* Desktop table */}
|
||||
<table className="hidden w-full sm:table">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
<th className="px-4 py-3 font-medium">Horario</th>
|
||||
<th className="px-4 py-3 font-medium">Cancha</th>
|
||||
<th className="px-4 py-3 font-medium">Cliente</th>
|
||||
<th className="hidden px-4 py-3 font-medium md:table-cell">Teléfono</th>
|
||||
<th className="px-4 py-3 font-medium">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{reservedSegments.map(({ schedule, segment }) => (
|
||||
<BookingRow
|
||||
key={segment.id}
|
||||
segment={segment}
|
||||
courtName={schedule.court.name}
|
||||
sportName={schedule.court.sport.name}
|
||||
onClick={() => openBookingTools(segment)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Mobile cards */}
|
||||
<div className="divide-y sm:hidden">
|
||||
{reservedSegments.map(({ schedule, segment }) => (
|
||||
<button
|
||||
key={segment.id}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-4 px-4 py-4 text-left transition-colors hover:bg-muted/50"
|
||||
onClick={() => openBookingTools(segment)}
|
||||
>
|
||||
<div className="flex size-12 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-secondary/40 text-muted-foreground">
|
||||
<Clock className="size-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold">
|
||||
{segment.booking?.customerName ?? 'Sin información'}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{schedule.court.name} · {schedule.court.sport.name}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{segment.startTime} - {segment.endTime}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge status={segment.booking?.status ?? 'CONFIRMED'} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface BookingRowProps {
|
||||
segment: BookingTimelineSegment;
|
||||
courtName: string;
|
||||
sportName: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function BookingRow({ segment, courtName, sportName, onClick }: BookingRowProps) {
|
||||
const booking = segment.booking;
|
||||
|
||||
return (
|
||||
<tr
|
||||
className="cursor-pointer transition-colors hover:bg-muted/50"
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">
|
||||
{segment.startTime} - {segment.endTime}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="size-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{courtName}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{sportName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate text-sm">{booking?.customerName ?? 'Sin información'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="hidden px-4 py-4 md:table-cell">
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">{booking?.customerPhone ?? '-'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<StatusBadge status={booking?.status ?? 'CONFIRMED'} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const config = statusConfig[status] ?? {
|
||||
label: status,
|
||||
className: 'bg-slate-500/15 text-slate-600 dark:text-slate-400 border-slate-500/30',
|
||||
};
|
||||
const Icon = statusIconConfig[status] ?? ShieldCheck;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-semibold',
|
||||
config.className
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
Dumbbell,
|
||||
Lock,
|
||||
MapPin,
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
Moon,
|
||||
Share2,
|
||||
ShieldCheck,
|
||||
Shirt,
|
||||
Sparkles,
|
||||
Sun,
|
||||
Trophy,
|
||||
@@ -414,7 +412,7 @@ function PublicBookingPageChrome({
|
||||
if (mobile) {
|
||||
return (
|
||||
<main className={`${themeClass} min-h-screen bg-[#050d14] text-white`}>
|
||||
<div className="public-booking-frame mx-auto min-h-screen max-w-[430px] bg-[radial-gradient(circle_at_50%_0%,rgba(17,185,129,0.16),transparent_34%),linear-gradient(180deg,#07131d_0%,#071018_47%,#050b11_100%)] px-4 pb-24 pt-7 shadow-2xl shadow-black">
|
||||
<div className="public-booking-frame mx-auto min-h-screen max-w-[430px] bg-[radial-gradient(circle_at_50%_0%,rgba(17,185,129,0.16),transparent_34%),linear-gradient(180deg,#07131d_0%,#071018_47%,#050b11_100%)] px-4 pb-0 pt-7 shadow-2xl shadow-black">
|
||||
<header className="flex items-center justify-between">
|
||||
<PlayzerBrand compact />
|
||||
<PublicBookingThemeToggle />
|
||||
@@ -441,8 +439,6 @@ function PublicBookingPageChrome({
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
<MobileBottomNav />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@@ -553,8 +549,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
||||
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 currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate);
|
||||
const canGoBackButton = canGoBack || currentDayIndex > 0;
|
||||
const canGoBackButton = canGoBack;
|
||||
|
||||
return (
|
||||
<PublicBookingPageChrome
|
||||
@@ -683,10 +678,7 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(340px,0.95fr)] gap-4">
|
||||
<CourtDetails court={selectedCourt} />
|
||||
<SelectedSlotCard {...props} />
|
||||
</div>
|
||||
<SelectedSlotCard {...props} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
@@ -1161,41 +1153,6 @@ function BookingTimeline({
|
||||
);
|
||||
}
|
||||
|
||||
function CourtDetails({ court }: { court?: PublicAvailabilityCourt }) {
|
||||
return (
|
||||
<Panel className="p-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="hidden size-24 items-center justify-center rounded border border-white/16 text-white/45 md:flex">
|
||||
<MapPin className="size-14" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{court?.courtName ? `${court.courtName} de ${court.sport.name}` : 'Cancha'}
|
||||
</h3>
|
||||
<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: Césped sintético
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Dumbbell className="size-4 text-emerald-400" />
|
||||
Turnos de {court?.slotDurationMinutes ?? 60} minutos
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Sparkles className="size-4 text-emerald-400" />
|
||||
Iluminación LED
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Shirt className="size-4 text-emerald-400" />
|
||||
Vestuarios disponibles
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
||||
const {
|
||||
selectedSlot,
|
||||
@@ -1217,9 +1174,11 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
||||
useEffect(() => {
|
||||
if (!selectedSlot) return;
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
customerNameInputRef.current?.focus();
|
||||
});
|
||||
const input = customerNameInputRef.current;
|
||||
if (!input) return;
|
||||
|
||||
input.focus();
|
||||
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, [selectedSlot]);
|
||||
|
||||
return (
|
||||
@@ -1322,40 +1281,6 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<nav className="fixed inset-x-0 bottom-0 z-40 mx-auto max-w-[430px] border-t border-emerald-500/30 bg-[#061019]/94 px-4 py-3 backdrop-blur">
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={item.label}
|
||||
href={
|
||||
item.label === 'Reservar' ? '#' : `#${item.label.toLowerCase().replace(' ', '-')}`
|
||||
}
|
||||
className={`flex flex-col items-center gap-1 text-[11px] ${
|
||||
item.active ? 'text-emerald-400' : 'text-white/58'
|
||||
}`}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
{item.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
@@ -1579,7 +1504,8 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
||||
visibleCourts.length,
|
||||
]);
|
||||
|
||||
const canGoBack = windowStartOffset > 0;
|
||||
const currentDayIndex = dayOptions.findIndex((option) => option.value === selectedDate);
|
||||
const canGoBack = windowStartOffset > 0 || currentDayIndex > 0;
|
||||
|
||||
const selectDate = (date: string) => {
|
||||
setSelectedDate(date);
|
||||
|
||||
Reference in New Issue
Block a user