feat: enhance sport selection UI in public booking page
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -15,10 +15,12 @@
|
|||||||
"@better-auth/infra": "^0.2.4",
|
"@better-auth/infra": "^0.2.4",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@repo/api-contract": "workspace:*",
|
"@repo/api-contract": "workspace:*",
|
||||||
|
"@tabler/icons-react": "^3.41.1",
|
||||||
"@tanstack/react-query": "^5.96.1",
|
"@tanstack/react-query": "^5.96.1",
|
||||||
"@tanstack/react-query-devtools": "^5.96.1",
|
"@tanstack/react-query-devtools": "^5.96.1",
|
||||||
"@tanstack/react-router": "^1.168.10",
|
"@tanstack/react-router": "^1.168.10",
|
||||||
|
|||||||
32
apps/frontend/src/assets/icons/football.tsx
Normal file
32
apps/frontend/src/assets/icons/football.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { type LucideProps } from 'lucide-react';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const SoccerBallIcon = forwardRef<any, LucideProps>(({ className, ...props }, ref) => (
|
||||||
|
<svg
|
||||||
|
ref={ref}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 2c.8 2.2 2.2 4.2 4 5.8" />
|
||||||
|
<path d="M12 2c-.8 2.2-2.2 4.2-4 5.8" />
|
||||||
|
<path d="M2 12c2.2.8 4.2 2.2 5.8 4" />
|
||||||
|
<path d="M2 12c2.2-.8 4.2-2.2 5.8-4" />
|
||||||
|
<path d="M22 12c-2.2.8-4.2 2.2-5.8 4" />
|
||||||
|
<path d="M22 12c-2.2-.8-4.2-2.2-5.8-4" />
|
||||||
|
<path d="M12 22c.8-2.2 2.2-4.2 4-5.8" />
|
||||||
|
<path d="M12 22c-.8-2.2-2.2-4.2-4-5.8" />
|
||||||
|
</svg>
|
||||||
|
));
|
||||||
|
|
||||||
|
export { SoccerBallIcon as FootballIcon };
|
||||||
@@ -1,20 +1,89 @@
|
|||||||
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 { ApiClientError, apiClient } from '@/lib/api-client';
|
import { ApiClientError, apiClient } from '@/lib/api-client';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { Dumbbell } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import {SoccerBallIcon, RacquetIcon, TennisBallIcon,BasketballIcon, VolleyballIcon, BarbellIcon, PersonSimpleRunIcon, PingPongIcon} from '@phosphor-icons/react'
|
||||||
|
|
||||||
|
const sportConfig: Record<
|
||||||
|
string,
|
||||||
|
{ icon: typeof Dumbbell; color: string; bg: string; border: string; label: string }
|
||||||
|
> = {
|
||||||
|
tennis: {
|
||||||
|
icon: TennisBallIcon,
|
||||||
|
color: 'text-green-700 dark:text-green-400',
|
||||||
|
bg: 'bg-green-50 dark:bg-green-950/40',
|
||||||
|
border: 'border-green-200 dark:border-green-800',
|
||||||
|
label: 'Tenis',
|
||||||
|
},
|
||||||
|
futbol: {
|
||||||
|
icon: SoccerBallIcon,
|
||||||
|
color: 'text-emerald-700 dark:text-emerald-400',
|
||||||
|
bg: 'bg-emerald-50 dark:bg-emerald-950/40',
|
||||||
|
border: 'border-emerald-200 dark:border-emerald-800',
|
||||||
|
label: 'Fútbol',
|
||||||
|
},
|
||||||
|
paddle: {
|
||||||
|
icon: RacquetIcon,
|
||||||
|
color: 'text-amber-700 dark:text-amber-400',
|
||||||
|
bg: 'bg-amber-50 dark:bg-amber-950/40',
|
||||||
|
border: 'border-amber-200 dark:border-amber-800',
|
||||||
|
label: 'Pádel',
|
||||||
|
},
|
||||||
|
basketball: {
|
||||||
|
icon: BasketballIcon,
|
||||||
|
color: 'text-orange-700 dark:text-orange-400',
|
||||||
|
bg: 'bg-orange-50 dark:bg-orange-950/40',
|
||||||
|
border: 'border-orange-200 dark:border-orange-800',
|
||||||
|
label: 'Básquet',
|
||||||
|
},
|
||||||
|
volleyball: {
|
||||||
|
icon: VolleyballIcon,
|
||||||
|
color: 'text-sky-700 dark:text-sky-400',
|
||||||
|
bg: 'bg-sky-50 dark:bg-sky-950/40',
|
||||||
|
border: 'border-sky-200 dark:border-sky-800',
|
||||||
|
label: 'Vóley',
|
||||||
|
},
|
||||||
|
gym: {
|
||||||
|
icon: BarbellIcon,
|
||||||
|
color: 'text-slate-700 dark:text-slate-400',
|
||||||
|
bg: 'bg-slate-50 dark:bg-slate-950/40',
|
||||||
|
border: 'border-slate-200 dark:border-slate-800',
|
||||||
|
label: 'Gimnasio',
|
||||||
|
},
|
||||||
|
pickleball: {
|
||||||
|
icon: RacquetIcon,
|
||||||
|
color: 'text-lime-700 dark:text-lime-400',
|
||||||
|
bg: 'bg-lime-50 dark:bg-lime-950/40',
|
||||||
|
border: 'border-lime-200 dark:border-lime-800',
|
||||||
|
label: 'Pickleball',
|
||||||
|
},
|
||||||
|
pingpong: {
|
||||||
|
icon: PingPongIcon,
|
||||||
|
color: 'text-blue-700 dark:text-blue-400',
|
||||||
|
bg: 'bg-blue-50 dark:bg-blue-950/40',
|
||||||
|
border: 'border-blue-200 dark:border-blue-800',
|
||||||
|
label: 'Ping Pong',
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
icon: PersonSimpleRunIcon,
|
||||||
|
color: 'text-violet-700 dark:text-violet-400',
|
||||||
|
bg: 'bg-violet-50 dark:bg-violet-950/40',
|
||||||
|
border: 'border-violet-200 dark:border-violet-800',
|
||||||
|
label: 'Deporte',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSportConfig(slug: string) {
|
||||||
|
const key = Object.keys(sportConfig).find((k) => slug.toLowerCase().includes(k));
|
||||||
|
return sportConfig[key || 'default'];
|
||||||
|
}
|
||||||
|
|
||||||
const DAYS_STEP = 5;
|
const DAYS_STEP = 5;
|
||||||
|
|
||||||
@@ -138,6 +207,11 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (availability.sports.length === 1 && !selectedSportId) {
|
||||||
|
setSelectedSportId(availability.sports[0].id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedSportId && !availability.sports.some((sport) => sport.id === selectedSportId)) {
|
if (selectedSportId && !availability.sports.some((sport) => sport.id === selectedSportId)) {
|
||||||
setSelectedSportId(undefined);
|
setSelectedSportId(undefined);
|
||||||
}
|
}
|
||||||
@@ -419,28 +493,36 @@ export function PublicBookingPage({ complexSlug }: PublicBookingPageProps) {
|
|||||||
|
|
||||||
{availabilityQuery.data?.sportSelectionRequired && (
|
{availabilityQuery.data?.sportSelectionRequired && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Field>
|
<h2 className="text-sm font-medium mb-3">Elegí tu deporte</h2>
|
||||||
<FieldLabel>Deporte</FieldLabel>
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
<Select
|
{availabilityQuery.data?.sports.map((sport) => {
|
||||||
value={selectedSportId ?? ''}
|
const config = getSportConfig(sport.slug);
|
||||||
onValueChange={(value) => {
|
const Icon = config.icon;
|
||||||
setSelectedSportId(value);
|
const isSelected = selectedSportId === sport.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={sport.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSportId(sport.id);
|
||||||
setSelectedSlot(null);
|
setSelectedSlot(null);
|
||||||
setAutoAdjustedDateNotice(null);
|
setAutoAdjustedDateNotice(null);
|
||||||
}}
|
}}
|
||||||
|
className={`relative flex flex-col items-center gap-2 rounded-xl border-2 p-4 transition-all hover:scale-[1.02] ${
|
||||||
|
isSelected
|
||||||
|
? `${config.border} ${config.bg} ring-2 ring-offset-2 ring-primary/20`
|
||||||
|
: 'border-border bg-card hover:border-muted-foreground/30'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-10 w-full">
|
<Icon className={`size-8 ${isSelected ? config.color : 'text-muted-foreground'}`} />
|
||||||
<SelectValue placeholder="Selecciona un deporte" />
|
<span className={`text-sm font-medium ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}>
|
||||||
</SelectTrigger>
|
{config.label}
|
||||||
<SelectContent>
|
</span>
|
||||||
{availabilityQuery.data.sports.map((sport) => (
|
</button>
|
||||||
<SelectItem key={sport.id} value={sport.id}>
|
);
|
||||||
{sport.name}
|
})}
|
||||||
</SelectItem>
|
</div>
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
8
bun.lock
8
bun.lock
@@ -47,10 +47,12 @@
|
|||||||
"@better-auth/infra": "^0.2.4",
|
"@better-auth/infra": "^0.2.4",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@repo/api-contract": "workspace:*",
|
"@repo/api-contract": "workspace:*",
|
||||||
|
"@tabler/icons-react": "^3.41.1",
|
||||||
"@tanstack/react-query": "^5.96.1",
|
"@tanstack/react-query": "^5.96.1",
|
||||||
"@tanstack/react-query-devtools": "^5.96.1",
|
"@tanstack/react-query-devtools": "^5.96.1",
|
||||||
"@tanstack/react-router": "^1.168.10",
|
"@tanstack/react-router": "^1.168.10",
|
||||||
@@ -345,6 +347,8 @@
|
|||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
||||||
|
|
||||||
|
"@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="],
|
||||||
|
|
||||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||||
|
|
||||||
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.6.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.6.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw=="],
|
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.6.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.6.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-BjHNmJqqa42NqJSDPnXUfwUofWo8LJY7Ui2gqxN4DmAOb+H/gGKv+hln2Xq/1kSJXPW5AXMXuNiPDMpywvyIOw=="],
|
||||||
@@ -559,6 +563,10 @@
|
|||||||
|
|
||||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||||
|
|
||||||
|
"@tabler/icons": ["@tabler/icons@3.41.1", "", {}, "sha512-OaRnVbRmH2nHtFeg+RmMJ/7m2oBIF9XCJAUD5gQnMrpK9f05ydj8MZrAf3NZQqOXyxGN1UBL0D5IKLLEUfr74Q=="],
|
||||||
|
|
||||||
|
"@tabler/icons-react": ["@tabler/icons-react@3.41.1", "", { "dependencies": { "@tabler/icons": "3.41.1" }, "peerDependencies": { "react": ">= 16" } }, "sha512-kUgweE+DJtAlMZVIns1FTDdcbpRVnkK7ZpUOXmoxy3JAF0rSHj0TcP4VHF14+gMJGnF+psH2Zt26BLT6owetBA=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user