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.
2026-03-03 19:59:14 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useSession } from 'next-auth/react';
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
import { ReactNode, useEffect } from 'react';
|
|
|
|
|
import { useTenant } from '../contexts/tenant-context';
|
|
|
|
|
|
|
|
|
|
export function AuthGuard({ children }: { children: ReactNode }) {
|
2026-03-06 22:26:55 +01:00
|
|
|
const { status } = useSession();
|
2026-03-06 08:01:09 +01:00
|
|
|
const { activeClubId, clubs, setActiveClub, clubsLoading } = useTenant();
|
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.
2026-03-03 19:59:14 +01:00
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (status === 'unauthenticated') {
|
|
|
|
|
router.push('/login');
|
|
|
|
|
}
|
|
|
|
|
}, [status, router]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (status === 'authenticated' && clubs.length > 0) {
|
|
|
|
|
if (clubs.length === 1 && !activeClubId) {
|
|
|
|
|
setActiveClub(clubs[0].id);
|
|
|
|
|
} else if (clubs.length > 1 && !activeClubId) {
|
|
|
|
|
router.push('/select-club');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [status, clubs, activeClubId, router, setActiveClub]);
|
|
|
|
|
|
|
|
|
|
if (status === 'loading') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
|
|
|
<p>Loading...</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status === 'unauthenticated') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 08:01:09 +01:00
|
|
|
if (status === 'authenticated' && clubsLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
|
|
|
<p>Loading...</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
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.
2026-03-03 19:59:14 +01:00
|
|
|
if (clubs.length === 0 && status === 'authenticated') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
|
|
|
|
|
<h2 className="text-2xl font-bold">No Clubs Found</h2>
|
|
|
|
|
<p>Contact admin to get access to a club</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (clubs.length > 1 && !activeClubId) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <>{children}</>;
|
|
|
|
|
}
|