315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
import { FootballIcon } from '@/assets/icons/football';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { UserAvatar } from '@/components/user-avatar';
|
|
import { apiClient } from '@/lib/api-client';
|
|
import { useAuth } from '@/lib/auth';
|
|
import { acceptStoredPendingInvite } from '@/lib/invitations';
|
|
import { useCurrentComplexStore } from '@/lib/stores/current-complex-store';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { Link, Outlet, useNavigate } from '@tanstack/react-router';
|
|
import { LogOut, Menu, UserRound } from 'lucide-react';
|
|
import { useEffect, useMemo, useRef } from 'react';
|
|
import { ThemeSwitcher } from './theme-switcher';
|
|
|
|
export function RootLayout() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { user, isAuthenticated, displayName, avatarUrl, signOut } = useAuth();
|
|
const { currentComplexSlug, setCurrentComplex } = useCurrentComplexStore();
|
|
const processedInviteForUser = useRef<string | null>(null);
|
|
const myComplexesQuery = useQuery({
|
|
queryKey: ['my-complexes'],
|
|
enabled: isAuthenticated,
|
|
queryFn: () => apiClient.complexes.listMine(),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!isAuthenticated) {
|
|
processedInviteForUser.current = null;
|
|
return;
|
|
}
|
|
|
|
const userKey = user?.email ?? displayName ?? 'authenticated-user';
|
|
|
|
if (processedInviteForUser.current === userKey) {
|
|
return;
|
|
}
|
|
|
|
processedInviteForUser.current = userKey;
|
|
|
|
void (async () => {
|
|
await acceptStoredPendingInvite(apiClient.complexes.acceptPendingInvitations);
|
|
await queryClient.invalidateQueries({ queryKey: ['my-complexes'] });
|
|
})();
|
|
}, [displayName, isAuthenticated, queryClient, user?.email]);
|
|
|
|
const complexSlug = currentComplexSlug || myComplexesQuery.data?.[0]?.complexSlug;
|
|
|
|
const currentComplexName = useMemo(() => {
|
|
const complexes = myComplexesQuery.data ?? [];
|
|
if (complexes.length === 0) return 'Mi complejo';
|
|
|
|
const selected = complexes.find((complex) => complex.complexSlug === complexSlug);
|
|
|
|
return selected?.complexName ?? complexes[0]?.complexName ?? 'Mi complejo';
|
|
}, [complexSlug, myComplexesQuery.data]);
|
|
|
|
const handleSignOut = async () => {
|
|
await signOut();
|
|
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="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>
|
|
|
|
<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">
|
|
<ThemeSwitcher />
|
|
|
|
{!isAuthenticated && (
|
|
<Button asChild size="sm" variant="outline" className="hidden md:inline-flex">
|
|
<Link to="/login">Ingresar</Link>
|
|
</Button>
|
|
)}
|
|
|
|
{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>
|
|
)}
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon-sm"
|
|
className="lg:hidden"
|
|
aria-label="Abrir menú"
|
|
>
|
|
<Menu className="size-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<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 && (
|
|
<>
|
|
<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 />
|
|
</>
|
|
)}
|
|
{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>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex-1">
|
|
<Outlet />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|