feat(public-booking): refactor court selection and available slots components for improved usability

This commit is contained in:
Jose Selesan
2026-06-02 16:26:47 -03:00
parent bc4716c48f
commit 3e314a9b9a

View File

@@ -2,13 +2,6 @@ import PlayzerIcon from '@/assets/playzer-favicon-512-transparent.png';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Field, FieldError, FieldLabel } from '@/components/ui/field'; import { Field, FieldError, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { import {
Stepper, Stepper,
StepperIndicator, StepperIndicator,
@@ -716,15 +709,10 @@ function PublicBookingMobile(props: BookingShellProps) {
/> />
)} )}
{props.courts.length > 0 ? ( {props.courts.length > 0 ? (
<MobileSelect <MobileCourtPicker
label="Cancha" courts={props.courts}
value={selectedCourt?.courtId ?? ''} selectedCourtId={selectedCourt?.courtId}
placeholder="Seleccioná" onSelectCourt={props.onSelectCourt}
onValueChange={props.onSelectCourt}
options={props.courts.map((court) => ({
value: court.courtId,
label: court.courtName,
}))}
/> />
) : ( ) : (
!props.isLoading && ( !props.isLoading && (
@@ -772,16 +760,12 @@ function PublicBookingMobile(props: BookingShellProps) {
<Panel className="overflow-hidden"> <Panel className="overflow-hidden">
<div className="p-3"> <div className="p-3">
<CourtHeader court={selectedCourt} compact /> <CourtHeader court={selectedCourt} compact />
<div className="mt-3">
<Legend />
</div> </div>
</div> <div className="border-t border-white/10 p-3">
<div className="border-t border-white/10 px-0 py-3"> <MobileAvailableSlots
<BookingTimeline
court={selectedCourt} court={selectedCourt}
selectedSlot={props.selectedSlot} selectedSlot={props.selectedSlot}
onSelectSlot={props.onSelectSlot} onSelectSlot={props.onSelectSlot}
compact
/> />
</div> </div>
<div className="px-3 pb-3"> <div className="px-3 pb-3">
@@ -890,34 +874,118 @@ function MobileSportSelector({
); );
} }
function MobileSelect({ function MobileCourtPicker({
label, courts,
value, selectedCourtId,
placeholder, onSelectCourt,
options,
onValueChange,
}: { }: {
label: string; courts: PublicAvailabilityCourt[];
value: string; selectedCourtId?: string;
placeholder: string; onSelectCourt: (courtId: string) => void;
options: { value: string; label: string }[];
onValueChange: (value: string) => void;
}) { }) {
return ( 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"> <div className="border-b border-white/10 bg-white/[0.025] px-3 py-3 last:border-b-0">
<span className="text-xs text-white/66">{label}</span> <div className="grid grid-cols-2 gap-2">
<Select value={value} onValueChange={onValueChange}> {courts.map((court) => {
<SelectTrigger className="h-8 border-0 bg-transparent px-0 text-xs text-white shadow-none"> const isSelected = selectedCourtId === court.courtId;
<SelectValue placeholder={placeholder} />
</SelectTrigger> return (
<SelectContent> <button
{options.map((option) => ( key={court.courtId}
<SelectItem key={option.value} value={option.value}> type="button"
{option.label} onClick={() => onSelectCourt(court.courtId)}
</SelectItem> aria-pressed={isSelected}
))} className={`flex min-h-16 items-start gap-2 rounded-md border p-3 text-left transition ${
</SelectContent> isSelected
</Select> ? 'border-emerald-400 bg-emerald-500/20 text-white shadow-[inset_0_0_18px_rgba(16,185,129,0.12)]'
: 'border-white/10 bg-white/[0.045] text-white/72 active:border-white/25 active:bg-white/[0.075]'
}`}
>
<span
className={`mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border ${
isSelected
? 'border-emerald-300 bg-emerald-400 text-[#061019]'
: 'border-white/18 text-transparent'
}`}
>
<Check className="size-3.5" />
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-semibold">{court.courtName}</span>
<span className="mt-0.5 block truncate text-xs text-white/52">
{court.sport.name}
</span>
</span>
</button>
);
})}
</div>
</div>
);
}
function MobileAvailableSlots({
court,
selectedSlot,
onSelectSlot,
}: {
court: PublicAvailabilityCourt;
selectedSlot: SelectedSlot | null;
onSelectSlot: (slot: SelectedSlot) => void;
}) {
const slots = court.availableSlots;
return (
<div>
<div className="flex items-end justify-between gap-3">
<div>
<h3 className="text-base font-semibold">Horarios disponibles</h3>
<p className="mt-0.5 text-xs text-white/58">
Turnos de {court.slotDurationMinutes} minutos
</p>
</div>
<span className="shrink-0 text-xs font-medium text-emerald-300/90">
{slots.length} {slots.length === 1 ? 'turno' : 'turnos'}
</span>
</div>
{slots.length > 0 ? (
<div className="mt-3 grid grid-cols-3 gap-2">
{slots.map((slot) => {
const selected = isSlotSelected(slot, court.courtId, selectedSlot);
return (
<button
key={`${court.courtId}-${slot.startTime}`}
type="button"
onClick={() =>
onSelectSlot({
courtId: court.courtId,
courtName: court.courtName,
sportId: court.sport.id,
sportName: court.sport.name,
startTime: slot.startTime,
endTime: slot.endTime,
})
}
aria-pressed={selected}
className={`flex h-12 items-center justify-center gap-1.5 rounded-md border text-sm font-bold transition ${
selected
? 'border-emerald-300 bg-emerald-400 text-[#061019] shadow-lg shadow-emerald-500/20'
: 'border-emerald-500/45 bg-emerald-500/12 text-white active:bg-emerald-500/24'
}`}
>
{selected && <Check className="size-4" />}
{slot.startTime}
</button>
);
})}
</div>
) : (
<p className="mt-3 rounded-md border border-white/10 bg-white/[0.035] p-3 text-sm text-white/62">
No hay horarios disponibles para esta cancha.
</p>
)}
</div> </div>
); );
} }