feat: add city/state/country to complex, new settings page with sidebar, and Biome linting

- Added city, state, and country optional fields to Complex model
- Updated onboarding to include optional location fields
- Created new settings page with sidebar navigation (Datos del Complejo, Canchas)
- Replaced ESLint with Biome for frontend and backend linting
- Added parallel dev script with concurrently
- Migrated register-routes to use direct app.route() pattern
This commit is contained in:
Jose Selesan
2026-04-10 15:36:15 -03:00
parent 77004b14b0
commit 9ee98a4cb4
129 changed files with 2929 additions and 3186 deletions

View File

@@ -1,9 +1,5 @@
import { Link, Outlet, useNavigate } from '@tanstack/react-router'
import { useEffect, useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { LogOut, Menu, UserRound } from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -11,54 +7,56 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useAuth } from '@/lib/auth'
} from '@/components/ui/dropdown-menu';
import { apiClient } from '@/lib/api-client';
import { useAuth } from '@/lib/auth';
import {
getCurrentComplexSlug,
setCurrentComplexSlug as persistCurrentComplexSlug,
} from '@/lib/current-complex'
import { apiClient } from '@/lib/api-client'
import { ThemeSwitcher } from './theme-switcher'
} from '@/lib/current-complex';
import { useQuery } from '@tanstack/react-query';
import { Link, Outlet, useNavigate } from '@tanstack/react-router';
import { LogOut, Menu, UserRound } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { ThemeSwitcher } from './theme-switcher';
export function RootLayout() {
const navigate = useNavigate()
const { isAuthenticated, displayName, initials, avatarUrl, signOut } = useAuth()
const [currentComplexSlug, setCurrentComplexSlug] = useState<string | null>(null)
const navigate = useNavigate();
const { isAuthenticated, displayName, initials, avatarUrl, signOut } = useAuth();
const [currentComplexSlug, setCurrentComplexSlug] = useState<string | null>(null);
const myComplexesQuery = useQuery({
queryKey: ['my-complexes'],
enabled: isAuthenticated,
queryFn: () => apiClient.complexes.listMine(),
})
});
useEffect(() => {
setCurrentComplexSlug(getCurrentComplexSlug())
}, [])
setCurrentComplexSlug(getCurrentComplexSlug());
}, []);
useEffect(() => {
if (currentComplexSlug) return
if (currentComplexSlug) return;
const fallbackSlug = myComplexesQuery.data?.[0]?.complexSlug
const fallbackSlug = myComplexesQuery.data?.[0]?.complexSlug;
if (fallbackSlug) {
setCurrentComplexSlug(fallbackSlug)
persistCurrentComplexSlug(fallbackSlug)
setCurrentComplexSlug(fallbackSlug);
persistCurrentComplexSlug(fallbackSlug);
}
}, [currentComplexSlug, myComplexesQuery.data])
}, [currentComplexSlug, myComplexesQuery.data]);
const currentComplexName = useMemo(() => {
const complexes = myComplexesQuery.data ?? []
if (complexes.length === 0) return 'Mi complejo'
const complexes = myComplexesQuery.data ?? [];
if (complexes.length === 0) return 'Mi complejo';
const selected = complexes.find(
(complex) => complex.complexSlug === currentComplexSlug,
)
const selected = complexes.find((complex) => complex.complexSlug === currentComplexSlug);
return selected?.complexName ?? complexes[0]?.complexName ?? 'Mi complejo'
}, [currentComplexSlug, myComplexesQuery.data])
return selected?.complexName ?? complexes[0]?.complexName ?? 'Mi complejo';
}, [currentComplexSlug, myComplexesQuery.data]);
const handleSignOut = async () => {
await signOut()
await navigate({ to: '/login' })
}
await signOut();
await navigate({ to: '/login' });
};
return (
<div className="flex min-h-screen flex-col">
@@ -106,9 +104,7 @@ export function RootLayout() {
<AvatarImage src={avatarUrl ?? undefined} alt={displayName} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<span className="max-w-32 truncate text-sm text-foreground">
{displayName}
</span>
<span className="max-w-32 truncate text-sm text-foreground">{displayName}</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
@@ -116,7 +112,7 @@ export function RootLayout() {
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/profile' })
void navigate({ to: '/profile' });
}}
>
<UserRound className="size-4" />
@@ -125,7 +121,7 @@ export function RootLayout() {
<DropdownMenuItem
variant="destructive"
onSelect={() => {
void handleSignOut()
void handleSignOut();
}}
>
<LogOut className="size-4" />
@@ -152,14 +148,14 @@ export function RootLayout() {
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/' })
void navigate({ to: '/' });
}}
>
Home
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/about' })
void navigate({ to: '/about' });
}}
>
About
@@ -170,7 +166,7 @@ export function RootLayout() {
void navigate({
to: '/complex/$slug/edit',
params: { slug: currentComplexSlug },
})
});
}}
>
Configuración
@@ -182,7 +178,7 @@ export function RootLayout() {
<>
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/profile' })
void navigate({ to: '/profile' });
}}
>
<UserRound className="size-4" />
@@ -191,7 +187,7 @@ export function RootLayout() {
<DropdownMenuItem
variant="destructive"
onSelect={() => {
void handleSignOut()
void handleSignOut();
}}
>
<LogOut className="size-4" />
@@ -201,7 +197,7 @@ export function RootLayout() {
) : (
<DropdownMenuItem
onSelect={() => {
void navigate({ to: '/login' })
void navigate({ to: '/login' });
}}
>
Login
@@ -215,5 +211,5 @@ export function RootLayout() {
<Outlet />
</div>
</div>
)
);
}

View File

@@ -1,5 +1,4 @@
import { Laptop, Moon, Sun } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -8,21 +7,18 @@ import {
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTheme } from '@/lib/theme'
} from '@/components/ui/dropdown-menu';
import { useTheme } from '@/lib/theme';
import { Laptop, Moon, Sun } from 'lucide-react';
export function ThemeSwitcher() {
const { theme, resolvedTheme, setTheme } = useTheme()
const { theme, resolvedTheme, setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="outline" className="px-2">
{resolvedTheme === 'dark' ? (
<Moon className="size-4" />
) : (
<Sun className="size-4" />
)}
{resolvedTheme === 'dark' ? <Moon className="size-4" /> : <Sun className="size-4" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
@@ -47,5 +43,5 @@ export function ThemeSwitcher() {
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
);
}