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:
54
frontend/src/components/club-switcher.tsx
Normal file
54
frontend/src/components/club-switcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user