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
|
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { ClubSwitcher } from '../club-switcher';
|
|
|
|
|
import { useTenant } from '../../contexts/tenant-context';
|
|
|
|
|
|
|
|
|
|
vi.mock('../../contexts/tenant-context', () => ({
|
|
|
|
|
useTenant: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../ui/dropdown-menu', () => ({
|
|
|
|
|
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
2026-03-06 22:26:55 +01:00
|
|
|
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <div data-testid="trigger">{children}</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
|
|
|
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div data-testid="content">{children}</div>,
|
|
|
|
|
DropdownMenuItem: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => <div onClick={onClick} data-testid="menu-item">{children}</div>,
|
|
|
|
|
DropdownMenuLabel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
|
|
|
DropdownMenuSeparator: () => <div>---</div>,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
describe('ClubSwitcher', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders loading state when clubs is empty', () => {
|
2026-03-06 22:26:55 +01:00
|
|
|
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
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
|
|
|
activeClubId: null,
|
|
|
|
|
clubs: [],
|
|
|
|
|
setActiveClub: vi.fn(),
|
|
|
|
|
userRole: null
|
2026-03-06 22:26:55 +01:00
|
|
|
});
|
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
|
|
|
|
|
|
|
|
render(<ClubSwitcher />);
|
|
|
|
|
expect(screen.getByRole('button')).toHaveTextContent('Select Club');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders current club name and sport type badge', () => {
|
2026-03-06 22:26:55 +01:00
|
|
|
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
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
|
|
|
activeClubId: 'club-1',
|
|
|
|
|
clubs: [
|
|
|
|
|
{ id: 'club-1', name: 'Tennis Club', sportType: 'Tennis' },
|
|
|
|
|
{ id: 'club-2', name: 'Swim Club', sportType: 'Swimming' },
|
|
|
|
|
],
|
|
|
|
|
setActiveClub: vi.fn(),
|
|
|
|
|
userRole: 'admin'
|
2026-03-06 22:26:55 +01:00
|
|
|
});
|
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
|
|
|
|
|
|
|
|
render(<ClubSwitcher />);
|
|
|
|
|
expect(screen.getAllByText('Tennis Club')[0]).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calls setActiveClub when club is selected', () => {
|
|
|
|
|
const mockSetActiveClub = vi.fn();
|
2026-03-06 22:26:55 +01:00
|
|
|
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
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
|
|
|
activeClubId: 'club-1',
|
|
|
|
|
clubs: [
|
|
|
|
|
{ id: 'club-1', name: 'Tennis Club', sportType: 'Tennis' },
|
|
|
|
|
{ id: 'club-2', name: 'Swim Club', sportType: 'Swimming' },
|
|
|
|
|
],
|
|
|
|
|
setActiveClub: mockSetActiveClub,
|
|
|
|
|
userRole: 'admin'
|
2026-03-06 22:26:55 +01:00
|
|
|
});
|
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
|
|
|
|
|
|
|
|
render(<ClubSwitcher />);
|
|
|
|
|
|
|
|
|
|
const swimClub = screen.getByText('Swim Club');
|
|
|
|
|
fireEvent.click(swimClub);
|
|
|
|
|
|
|
|
|
|
expect(mockSetActiveClub).toHaveBeenCalledWith('club-2');
|
|
|
|
|
});
|
|
|
|
|
});
|