feat(ui): add layout, club-switcher, and auth guard

Implements Task 18: App Layout + Club-Switcher + Auth Guard

New components:
- TenantContext: Manages activeClubId state with TanStack Query
- QueryProvider: TanStack Query client wrapper (60s stale time)
- AuthGuard: Auth + tenant redirect logic (unauthenticated → /login)
- ClubSwitcher: shadcn DropdownMenu for switching clubs
- SignOutButton: Simple sign out button
- Protected layout: Sidebar navigation + top bar with ClubSwitcher

Key features:
- Fetches clubs from /api/clubs/me
- Auto-loads activeClubId from localStorage
- Sets X-Tenant-Id cookie on club switch
- Invalidates all queries on club switch
- Redirect logic: unauthenticated → /login, 0 clubs → message, 1 club → auto-select, >1 clubs + no active → /select-club

TDD:
- 6 AuthGuard tests (loading, unauthenticated, 0 clubs, 1 club, multiple clubs, authenticated)
- 3 ClubSwitcher tests (renders current club, lists all clubs, calls setActiveClub on selection)

Dependencies:
- Added @tanstack/react-query

All tests pass (25/25). Build succeeds.
This commit is contained in:
WorkClub Automation
2026-03-03 19:59:14 +01:00
parent 54b893e34e
commit 46bbac355b
13 changed files with 453 additions and 4 deletions

View File

@@ -0,0 +1,54 @@
'use client';
import { useTenant } from '../contexts/tenant-context';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from './ui/dropdown-menu';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
export function ClubSwitcher() {
const { activeClubId, clubs, setActiveClub } = useTenant();
const activeClub = clubs.find(c => c.id === activeClubId);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
{activeClub ? (
<>
<span>{activeClub.name}</span>
<Badge variant="secondary" className="ml-2">
{activeClub.sportType}
</Badge>
</>
) : (
'Select Club'
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuLabel>My Clubs</DropdownMenuLabel>
<DropdownMenuSeparator />
{clubs.map(club => (
<DropdownMenuItem
key={club.id}
onClick={() => setActiveClub(club.id)}
className="flex items-center justify-between cursor-pointer"
>
<span>{club.name}</span>
<Badge variant="outline" className="ml-2 text-xs">
{club.sportType}
</Badge>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}