Improve sport selector UI with visual button components

Replace dropdown select with interactive pill buttons for sport selection

- Added SportSelector component with icon-based visual buttons (desktop)
- Added MobileSportSelector component for mobile-optimized layout
- Added getSportIcon() function to map sport names to appropriate icons
- Improved visual feedback with emerald highlight on selection
- Better hover states and transitions for improved UX
- Works on both desktop and mobile views

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jose Selesan
2026-05-19 16:43:39 -03:00
parent e9bd759fa5
commit 91125e82f5

View File

@@ -47,6 +47,7 @@ import {
Trophy,
Users,
Wrench,
Zap,
} from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
@@ -250,6 +251,16 @@ function isSlotSelected(
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;
@@ -553,11 +564,9 @@ function PublicBookingDesktop(props: BookingShellProps) {
<h2 className="text-lg font-semibold">Seleccioná tu cancha</h2>
<div className="mt-5 space-y-4">
{sportSelectionRequired && (
<DarkSelect
<SportSelector
label="Deporte"
icon={<Trophy className="size-5" />}
value={selectedSportId ?? ''}
placeholder="Seleccioná"
onValueChange={onSelectSport}
options={sports.map((sport) => ({ value: sport.id, label: sport.name }))}
/>
@@ -693,12 +702,11 @@ function PublicBookingMobile(props: BookingShellProps) {
<div className="mt-5 space-y-3">
<Panel className="p-3">
<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 space-y-0">
{props.sportSelectionRequired && (
<MobileSelect
<MobileSportSelector
label="Deporte"
value={props.selectedSportId ?? ''}
placeholder="Seleccioná"
onValueChange={props.onSelectSport}
options={props.sports.map((sport) => ({ value: sport.id, label: sport.name }))}
/>
@@ -790,41 +798,84 @@ function StatusPanel({ children }: { children?: React.ReactNode }) {
);
}
function DarkSelect({
function SportSelector({
label,
icon,
value,
placeholder,
options,
onValueChange,
}: {
label: string;
icon: React.ReactNode;
value: string;
placeholder: string;
options: { value: string; label: string }[];
onValueChange: (value: string) => void;
}) {
return (
<Field>
<FieldLabel className="text-sm text-white/76">{label}</FieldLabel>
<Select value={value} onValueChange={onValueChange}>
<SelectTrigger className="h-10 rounded-md border-white/10 bg-white/[0.035] text-white">
<span className="mr-2 text-white">{icon}</span>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex flex-wrap gap-2">
{options.map((option) => {
const Icon = getSportIcon(option.label);
const isSelected = value === option.value;
return (
<button
key={option.value}
type="button"
onClick={() => onValueChange(option.value)}
className={`inline-flex items-center gap-2 rounded-full px-4 py-2 transition-all ${
isSelected
? 'border border-emerald-500 bg-emerald-500/18 shadow-[inset_0_0_20px_rgba(16,185,129,0.14)] text-white'
: 'border border-white/15 bg-white/[0.05] text-white/76 hover:border-white/30 hover:bg-white/[0.08] hover:text-white'
}`}
>
<Icon className="size-4" />
<span className="text-sm font-medium">{option.label}</span>
</button>
);
})}
</div>
</Field>
);
}
function MobileSportSelector({
label,
value,
options,
onValueChange,
}: {
label: string;
value: string;
options: { value: string; label: string }[];
onValueChange: (value: string) => void;
}) {
return (
<div className="border-b border-white/10 bg-white/[0.025] px-3 py-3 last:border-b-0">
<span className="mb-2 block text-xs text-white/66">{label}</span>
<div className="flex flex-wrap gap-2">
{options.map((option) => {
const Icon = getSportIcon(option.label);
const isSelected = value === option.value;
return (
<button
key={option.value}
type="button"
onClick={() => onValueChange(option.value)}
className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-all ${
isSelected
? 'border border-emerald-500 bg-emerald-500/18 shadow-[inset_0_0_15px_rgba(16,185,129,0.1)] text-white'
: 'border border-white/15 bg-white/[0.05] text-white/66 hover:border-white/25 hover:bg-white/[0.08] hover:text-white/80'
}`}
>
<Icon className="size-3.5" />
<span>{option.label}</span>
</button>
);
})}
</div>
</div>
);
}
function MobileSelect({
label,
value,