feat: enhance public booking page with improved layout and date formatting

- Added new date formatting function for better user experience.
- Updated layout of the public booking page for improved responsiveness and aesthetics.
- Integrated sport selection with visual icons for better clarity.
- Enhanced error handling and loading states for availability queries.

feat: revamp select complex page for better user interaction

- Redesigned the select complex page layout for a more modern look.
- Added contextual information about complex selection.
- Improved button interactions with loading indicators and icons.

style: update CSS variables for a cohesive sports-club theme

- Adjusted color variables to reflect a sports-club identity.
- Enhanced background styles for both light and dark modes.
- Improved overall styling consistency across components.
This commit is contained in:
Jose Selesan
2026-04-24 14:47:39 -03:00
parent c63b5a0a07
commit f1a7e6c24a
17 changed files with 1577 additions and 1048 deletions

View File

@@ -1,3 +1,4 @@
import { FootballIcon } from '@/assets/icons/football';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -66,59 +67,194 @@ export function RootLayout() {
await navigate({ to: '/login' });
};
type NavigationItem = {
label: string;
to: '/' | '/profile' | '/complex/$slug/edit';
show: boolean;
params?: { slug: string };
};
const navigationItems: NavigationItem[] = [
{
label: 'Inicio',
to: '/',
show: true,
},
{
label: 'Configuración',
to: '/complex/$slug/edit',
params: currentComplexSlug ? { slug: currentComplexSlug } : undefined,
show: isAuthenticated && Boolean(currentComplexSlug),
},
{
label: 'Perfil',
to: '/profile',
show: isAuthenticated,
},
] as const;
return (
<div className="flex min-h-screen flex-col">
<header className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-6">
<h1 className="text-sm font-semibold">{currentComplexName}</h1>
<nav className="hidden items-center gap-3 text-sm md:flex">
<Link
to="/"
className="text-muted-foreground"
activeProps={{ className: 'text-foreground font-medium' }}
>
Home
<header className="sticky top-0 z-40 border-b border-border/70 bg-background/80 backdrop-blur-xl">
<div className="mx-auto flex w-full max-w-7xl items-center justify-between gap-4 px-4 py-3 sm:px-6">
<div className="flex min-w-0 items-center gap-4">
<Link to="/" className="flex min-w-0 items-center gap-3">
<span className="relative flex size-10 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm ring-1 ring-primary/20">
<FootballIcon className="size-5" />
<span className="absolute -right-0.5 -bottom-0.5 size-2 rounded-full border border-background bg-accent" />
</span>
<span className="min-w-0">
<span className="block text-[10px] font-semibold uppercase tracking-[0.32em] text-muted-foreground">
Playzer
</span>
<span className="block truncate text-sm font-semibold text-foreground">
{currentComplexName}
</span>
</span>
</Link>
{isAuthenticated && currentComplexSlug && (
<Link
to="/complex/$slug/edit"
params={{ slug: currentComplexSlug }}
className="text-muted-foreground"
activeProps={{ className: 'text-foreground font-medium' }}
>
Configuración
</Link>
<nav className="hidden items-center gap-1 lg:flex">
{navigationItems
.filter((item) => item.show)
.map((item) =>
item.params ? (
<Link
key={item.label}
to={item.to}
params={item.params}
className="rounded-full px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
activeProps={{ className: 'bg-muted text-foreground font-medium' }}
>
{item.label}
</Link>
) : (
<Link
key={item.label}
to={item.to}
className="rounded-full px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
activeProps={{ className: 'bg-muted text-foreground font-medium' }}
>
{item.label}
</Link>
)
)}
</nav>
</div>
<div className="flex items-center gap-2">
<div className="hidden items-center gap-2 rounded-full border border-border/70 bg-card px-3 py-1.5 text-xs text-muted-foreground shadow-sm md:flex">
<span className="size-2 rounded-full bg-emerald-500" />
{isAuthenticated ? 'Sesión activa' : 'Modo visitante'}
</div>
<ThemeSwitcher />
{!isAuthenticated && (
<Button asChild size="sm" variant="outline" className="hidden md:inline-flex">
<Link to="/login">Ingresar</Link>
</Button>
)}
</nav>
</div>
<div className="flex items-center gap-2">
<ThemeSwitcher />
{isAuthenticated && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="inline-flex items-center gap-2 rounded-full border border-border/70 bg-card px-2 py-1.5 transition-colors hover:bg-muted"
>
<UserAvatar
src={avatarUrl}
alt={displayName}
fallbackText={displayName}
size="sm"
/>
<span className="hidden max-w-32 truncate text-sm text-foreground sm:block">
{displayName}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Mi cuenta</DropdownMenuLabel>
<DropdownMenuSeparator />
{myComplexesQuery.data && myComplexesQuery.data.length > 1 && (
<>
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
Cambiar complejo
</DropdownMenuLabel>
{myComplexesQuery.data.map((complex) => (
<DropdownMenuItem
key={complex.id}
onSelect={async () => {
await apiClient.complexes.select({ complexId: complex.id });
setCurrentComplex(complex.complexSlug);
}}
>
{complex.complexSlug === complexSlug && <span className="mr-2"></span>}
{complex.complexName}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/profile' });
}}
>
<UserRound className="size-4" />
Perfil
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => {
void handleSignOut();
}}
>
<LogOut className="size-4" />
Cerrar sesión
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{!isAuthenticated && (
<Button asChild size="sm" variant="outline" className="hidden md:inline-flex">
<Link to="/login">Login</Link>
</Button>
)}
{isAuthenticated && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
<Button
type="button"
className="hidden items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-muted md:inline-flex"
variant="outline"
size="icon-sm"
className="lg:hidden"
aria-label="Abrir menú"
>
<UserAvatar
src={avatarUrl}
alt={displayName}
fallbackText={displayName}
size="sm"
/>
<span className="max-w-32 truncate text-sm text-foreground">{displayName}</span>
</button>
<Menu className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuLabel>Mi cuenta</DropdownMenuLabel>
<DropdownMenuContent align="end" className="w-64 lg:hidden">
<DropdownMenuLabel>{currentComplexName}</DropdownMenuLabel>
<DropdownMenuSeparator />
{navigationItems
.filter((item) => item.show)
.map((item) =>
item.params ? (
<DropdownMenuItem
key={item.label}
onSelect={() => {
void navigate({ to: item.to, params: item.params });
}}
>
{item.label}
</DropdownMenuItem>
) : (
<DropdownMenuItem
key={item.label}
onSelect={() => {
void navigate({ to: item.to });
}}
>
{item.label}
</DropdownMenuItem>
)
)}
<DropdownMenuSeparator />
{myComplexesQuery.data && myComplexesQuery.data.length > 1 && (
<>
@@ -140,103 +276,41 @@ export function RootLayout() {
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/profile' });
}}
>
<UserRound className="size-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => {
void handleSignOut();
}}
>
<LogOut className="size-4" />
Sign out
</DropdownMenuItem>
{isAuthenticated ? (
<>
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/profile' });
}}
>
<UserRound className="size-4" />
Perfil
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => {
void handleSignOut();
}}
>
<LogOut className="size-4" />
Cerrar sesión
</DropdownMenuItem>
</>
) : (
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/login' });
}}
>
Ingresar
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon-sm"
className="md:hidden"
aria-label="Abrir menú"
>
<Menu className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 md:hidden">
<DropdownMenuLabel>Navegación</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/' });
}}
>
Home
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/about' });
}}
>
About
</DropdownMenuItem>
{isAuthenticated && currentComplexSlug && (
<DropdownMenuItem
onSelect={() => {
void navigate({
to: '/complex/$slug/edit',
params: { slug: currentComplexSlug },
});
}}
>
Configuración
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{isAuthenticated ? (
<>
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/profile' });
}}
>
<UserRound className="size-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => {
void handleSignOut();
}}
>
<LogOut className="size-4" />
Sign out
</DropdownMenuItem>
</>
) : (
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/login' });
}}
>
Login
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</header>
<div className="flex-1">
<Outlet />
</div>