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:
@@ -47,6 +47,7 @@ import {
|
|||||||
Trophy,
|
Trophy,
|
||||||
Users,
|
Users,
|
||||||
Wrench,
|
Wrench,
|
||||||
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@@ -250,6 +251,16 @@ function isSlotSelected(
|
|||||||
return selectedSlot?.courtId === courtId && selectedSlot.startTime === slot.startTime;
|
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() {
|
function PublicBookingThemeToggle() {
|
||||||
const { resolvedTheme, toggleTheme } = useTheme();
|
const { resolvedTheme, toggleTheme } = useTheme();
|
||||||
const Icon = resolvedTheme === 'dark' ? Sun : Moon;
|
const Icon = resolvedTheme === 'dark' ? Sun : Moon;
|
||||||
@@ -553,11 +564,9 @@ function PublicBookingDesktop(props: BookingShellProps) {
|
|||||||
<h2 className="text-lg font-semibold">Seleccioná 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
|
<SportSelector
|
||||||
label="Deporte"
|
label="Deporte"
|
||||||
icon={<Trophy className="size-5" />}
|
|
||||||
value={selectedSportId ?? ''}
|
value={selectedSportId ?? ''}
|
||||||
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 }))}
|
||||||
/>
|
/>
|
||||||
@@ -693,12 +702,11 @@ function PublicBookingMobile(props: BookingShellProps) {
|
|||||||
<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">Seleccioná 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 space-y-0">
|
||||||
{props.sportSelectionRequired && (
|
{props.sportSelectionRequired && (
|
||||||
<MobileSelect
|
<MobileSportSelector
|
||||||
label="Deporte"
|
label="Deporte"
|
||||||
value={props.selectedSportId ?? ''}
|
value={props.selectedSportId ?? ''}
|
||||||
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 }))}
|
||||||
/>
|
/>
|
||||||
@@ -790,41 +798,84 @@ function StatusPanel({ children }: { children?: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DarkSelect({
|
function SportSelector({
|
||||||
label,
|
label,
|
||||||
icon,
|
|
||||||
value,
|
value,
|
||||||
placeholder,
|
|
||||||
options,
|
options,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ReactNode;
|
|
||||||
value: string;
|
value: string;
|
||||||
placeholder: string;
|
|
||||||
options: { value: string; label: string }[];
|
options: { value: string; label: string }[];
|
||||||
onValueChange: (value: string) => void;
|
onValueChange: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel className="text-sm text-white/76">{label}</FieldLabel>
|
<FieldLabel className="text-sm text-white/76">{label}</FieldLabel>
|
||||||
<Select value={value} onValueChange={onValueChange}>
|
<div className="flex flex-wrap gap-2">
|
||||||
<SelectTrigger className="h-10 rounded-md border-white/10 bg-white/[0.035] text-white">
|
{options.map((option) => {
|
||||||
<span className="mr-2 text-white">{icon}</span>
|
const Icon = getSportIcon(option.label);
|
||||||
<SelectValue placeholder={placeholder} />
|
const isSelected = value === option.value;
|
||||||
</SelectTrigger>
|
return (
|
||||||
<SelectContent>
|
<button
|
||||||
{options.map((option) => (
|
key={option.value}
|
||||||
<SelectItem key={option.value} value={option.value}>
|
type="button"
|
||||||
{option.label}
|
onClick={() => onValueChange(option.value)}
|
||||||
</SelectItem>
|
className={`inline-flex items-center gap-2 rounded-full px-4 py-2 transition-all ${
|
||||||
))}
|
isSelected
|
||||||
</SelectContent>
|
? 'border border-emerald-500 bg-emerald-500/18 shadow-[inset_0_0_20px_rgba(16,185,129,0.14)] text-white'
|
||||||
</Select>
|
: '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>
|
</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({
|
function MobileSelect({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
|
|||||||
Reference in New Issue
Block a user