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

@@ -6,6 +6,7 @@
"name": "frontend",
"dependencies": {
"@auth/core": "^0.34.3",
"@tanstack/react-query": "^5.90.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.576.0",
@@ -543,6 +544,10 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],