From 0db80fa98de151272c3da9364db418fac4d7df11 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 7 Jan 2026 12:40:52 +0100 Subject: [PATCH] page wrapper --- apps/website/AUTH_FIXES_SUMMARY.md | 203 ---- apps/website/CLEAN_ARCHITECTURE_PLAN.md | 1003 +++++++++++++++++ apps/website/DEVELOPER_EXPERIENCE_SUMMARY.md | 227 ---- apps/website/ERROR_HANDLING_GUIDE.md | 224 ---- apps/website/PROTECTION_STRATEGY.md | 357 ------ apps/website/app/auth/login/page.tsx | 234 ++-- apps/website/app/auth/signup/page.tsx | 158 +-- apps/website/app/dashboard/page.tsx | 350 +----- .../app/drivers/DriversInteractive.tsx | 53 - apps/website/app/drivers/DriversStatic.tsx | 24 - .../drivers/[id]/DriverProfileInteractive.tsx | 169 --- .../app/drivers/[id]/DriverProfileStatic.tsx | 37 - apps/website/app/drivers/[id]/page.tsx | 82 +- apps/website/app/drivers/page.tsx | 15 +- .../leaderboards/LeaderboardsInteractive.tsx | 42 - .../app/leaderboards/LeaderboardsStatic.tsx | 80 -- .../drivers/DriverRankingsInteractive.tsx | 46 - .../drivers/DriverRankingsStatic.tsx | 70 -- .../website/app/leaderboards/drivers/page.tsx | 98 +- apps/website/app/leaderboards/page.tsx | 110 +- .../app/leagues/LeaguesInteractive.tsx | 53 - apps/website/app/leagues/LeaguesStatic.tsx | 32 - .../leagues/[id]/LeagueDetailInteractive.tsx | 111 -- .../app/leagues/[id]/LeagueDetailStatic.tsx | 60 - apps/website/app/leagues/[id]/layout.tsx | 4 +- apps/website/app/leagues/[id]/page.tsx | 94 +- .../rulebook/LeagueRulebookInteractive.tsx | 48 - .../[id]/rulebook/LeagueRulebookStatic.tsx | 38 - .../app/leagues/[id]/rulebook/page.tsx | 64 +- .../schedule/LeagueScheduleInteractive.tsx | 11 - .../[id]/schedule/LeagueScheduleStatic.tsx | 10 - .../app/leagues/[id]/schedule/admin/page.tsx | 432 +++---- .../app/leagues/[id]/schedule/page.tsx | 85 +- .../app/leagues/[id]/settings/page.tsx | 6 +- .../app/leagues/[id]/sponsorships/page.tsx | 132 +-- .../standings/LeagueStandingsInteractive.tsx | 98 -- .../[id]/standings/LeagueStandingsStatic.tsx | 71 -- .../app/leagues/[id]/standings/page.tsx | 108 +- .../[id]/stewarding/StewardingTemplate.tsx | 359 ++++++ .../app/leagues/[id]/stewarding/page.tsx | 405 +------ .../stewarding/protests/[protestId]/page.tsx | 6 +- .../leagues/[id]/wallet/WalletTemplate.tsx | 300 +++++ apps/website/app/leagues/[id]/wallet/page.tsx | 362 +----- apps/website/app/leagues/page.tsx | 20 +- apps/website/app/onboarding/page.tsx | 30 +- apps/website/app/page.tsx | 377 +------ apps/website/app/profile/leagues/page.tsx | 205 ++-- apps/website/app/profile/page.tsx | 286 +++-- apps/website/app/profile/settings/page.tsx | 28 +- .../app/profile/sponsorship-requests/page.tsx | 314 +----- apps/website/app/races/RacesInteractive.tsx | 180 --- apps/website/app/races/RacesStatic.tsx | 79 -- .../app/races/[id]/RaceDetailInteractive.tsx | 240 ---- apps/website/app/races/[id]/page.test.tsx | 238 ---- apps/website/app/races/[id]/page.tsx | 118 +- .../[id]/results/RaceResultsInteractive.tsx | 137 --- apps/website/app/races/[id]/results/page.tsx | 126 ++- .../stewarding/RaceStewardingInteractive.tsx | 87 -- .../app/races/[id]/stewarding/page.tsx | 141 ++- .../app/races/all/RacesAllInteractive.tsx | 99 -- apps/website/app/races/all/page.tsx | 115 +- apps/website/app/races/page.tsx | 72 +- .../website/app/sponsor/leagues/[id]/page.tsx | 560 +-------- apps/website/app/sponsor/leagues/page.tsx | 39 +- apps/website/app/teams/TeamsInteractive.tsx | 237 ---- apps/website/app/teams/TeamsStatic.tsx | 59 - .../app/teams/[id]/TeamDetailInteractive.tsx | 139 --- .../app/teams/[id]/TeamDetailStatic.tsx | 43 - apps/website/app/teams/[id]/page.tsx | 103 +- .../TeamLeaderboardInteractive.tsx | 45 - .../leaderboard/TeamLeaderboardStatic.tsx | 27 - apps/website/app/teams/leaderboard/page.tsx | 94 +- apps/website/app/teams/page.tsx | 98 +- .../components/leagues/LeagueSchedule.tsx | 4 +- .../components/leagues/MembershipStatus.tsx | 9 + .../components/shared/state/EmptyState.tsx | 26 +- .../components/shared/state/ErrorDisplay.tsx | 505 +++------ .../components/shared/state/PageWrapper.tsx | 276 +++++ .../shared/state/StateContainer.tsx | 11 +- .../shared/state/StatefulPageWrapper.tsx | 61 + apps/website/components/shared/state/types.ts | 98 +- .../hooks/dashboard/useDashboardOverview.ts | 20 - apps/website/hooks/driver/index.ts | 2 +- .../hooks/driver/useDriverLeaderboard.ts | 15 - .../hooks/driver/useDriverProfilePageData.ts | 47 + .../hooks/league/useAllLeaguesWithSponsors.ts | 15 - .../league/useLeagueDetailWithSponsors.ts | 21 - .../league/useLeagueScheduleAdminPageData.ts | 38 + .../league/useLeagueSponsorshipsPageData.ts | 21 + .../league/useLeagueStewardingMutations.ts | 46 + apps/website/hooks/league/useLeagueWallet.ts | 16 - .../hooks/league/useLeagueWalletPageData.ts | 47 + .../hooks/protest/useLeagueProtests.ts | 16 - .../website/hooks/race/useAllRacesPageData.ts | 12 + apps/website/hooks/race/useCancelRace.ts | 15 - apps/website/hooks/race/useCompleteRace.ts | 15 - apps/website/hooks/race/useRaceDetail.ts | 16 - .../hooks/race/useRaceResultsDetail.ts | 16 - .../hooks/race/useRaceResultsPageData.ts | 20 + .../hooks/race/useRaceStewardingData.ts | 16 - apps/website/hooks/race/useRaceWithSOF.ts | 16 - apps/website/hooks/race/useRacesPageData.ts | 15 - apps/website/hooks/race/useReopenRace.ts | 15 - .../sponsor/useSponsorshipRequestsPageData.ts | 120 ++ .../hooks/useLeagueMembershipService.ts | 16 - apps/website/hooks/useLeagueScoringPresets.ts | 12 +- apps/website/hooks/useLeagueWizardService.ts | 7 +- ...nalties.ts => usePenaltyTypesReference.ts} | 7 +- apps/website/lib/di/MIGRATION_SUMMARY.md | 280 ----- apps/website/lib/di/README.md | 177 --- apps/website/lib/hooks/useLeagueStewarding.ts | 100 -- apps/website/lib/hooks/useRaceResults.ts | 40 - apps/website/lib/hooks/useRaceStewarding.ts | 29 - apps/website/lib/hooks/useTeamLeaderboard.ts | 28 - apps/website/lib/page/PageDataFetcher.ts | 87 ++ apps/website/lib/page/usePageData.ts | 149 +++ apps/website/lib/services/home/getHomeData.ts | 28 + .../RaceResultsDataTransformer.ts | 105 ++ apps/website/lib/utils.ts | 15 + apps/website/templates/DashboardTemplate.tsx | 316 ++++++ apps/website/templates/DriversTemplate.tsx | 21 +- apps/website/templates/HomeTemplate.tsx | 355 ++++++ .../templates/LeagueAdminScheduleTemplate.tsx | 234 ++++ .../templates/LeagueScheduleTemplate.tsx | 232 +++- apps/website/templates/LeaguesTemplate.tsx | 60 +- .../templates/SponsorLeagueDetailTemplate.tsx | 578 ++++++++++ .../SponsorLeaguesTemplate.tsx} | 71 +- .../templates/SponsorshipRequestsTemplate.tsx | 158 +++ 128 files changed, 7386 insertions(+), 8096 deletions(-) delete mode 100644 apps/website/AUTH_FIXES_SUMMARY.md create mode 100644 apps/website/CLEAN_ARCHITECTURE_PLAN.md delete mode 100644 apps/website/DEVELOPER_EXPERIENCE_SUMMARY.md delete mode 100644 apps/website/ERROR_HANDLING_GUIDE.md delete mode 100644 apps/website/PROTECTION_STRATEGY.md delete mode 100644 apps/website/app/drivers/DriversInteractive.tsx delete mode 100644 apps/website/app/drivers/DriversStatic.tsx delete mode 100644 apps/website/app/drivers/[id]/DriverProfileInteractive.tsx delete mode 100644 apps/website/app/drivers/[id]/DriverProfileStatic.tsx delete mode 100644 apps/website/app/leaderboards/LeaderboardsInteractive.tsx delete mode 100644 apps/website/app/leaderboards/LeaderboardsStatic.tsx delete mode 100644 apps/website/app/leaderboards/drivers/DriverRankingsInteractive.tsx delete mode 100644 apps/website/app/leaderboards/drivers/DriverRankingsStatic.tsx delete mode 100644 apps/website/app/leagues/LeaguesInteractive.tsx delete mode 100644 apps/website/app/leagues/LeaguesStatic.tsx delete mode 100644 apps/website/app/leagues/[id]/LeagueDetailInteractive.tsx delete mode 100644 apps/website/app/leagues/[id]/LeagueDetailStatic.tsx delete mode 100644 apps/website/app/leagues/[id]/rulebook/LeagueRulebookInteractive.tsx delete mode 100644 apps/website/app/leagues/[id]/rulebook/LeagueRulebookStatic.tsx delete mode 100644 apps/website/app/leagues/[id]/schedule/LeagueScheduleInteractive.tsx delete mode 100644 apps/website/app/leagues/[id]/schedule/LeagueScheduleStatic.tsx delete mode 100644 apps/website/app/leagues/[id]/standings/LeagueStandingsInteractive.tsx delete mode 100644 apps/website/app/leagues/[id]/standings/LeagueStandingsStatic.tsx create mode 100644 apps/website/app/leagues/[id]/stewarding/StewardingTemplate.tsx create mode 100644 apps/website/app/leagues/[id]/wallet/WalletTemplate.tsx delete mode 100644 apps/website/app/races/RacesInteractive.tsx delete mode 100644 apps/website/app/races/RacesStatic.tsx delete mode 100644 apps/website/app/races/[id]/RaceDetailInteractive.tsx delete mode 100644 apps/website/app/races/[id]/page.test.tsx delete mode 100644 apps/website/app/races/[id]/results/RaceResultsInteractive.tsx delete mode 100644 apps/website/app/races/[id]/stewarding/RaceStewardingInteractive.tsx delete mode 100644 apps/website/app/races/all/RacesAllInteractive.tsx delete mode 100644 apps/website/app/teams/TeamsInteractive.tsx delete mode 100644 apps/website/app/teams/TeamsStatic.tsx delete mode 100644 apps/website/app/teams/[id]/TeamDetailInteractive.tsx delete mode 100644 apps/website/app/teams/[id]/TeamDetailStatic.tsx delete mode 100644 apps/website/app/teams/leaderboard/TeamLeaderboardInteractive.tsx delete mode 100644 apps/website/app/teams/leaderboard/TeamLeaderboardStatic.tsx create mode 100644 apps/website/components/shared/state/PageWrapper.tsx create mode 100644 apps/website/components/shared/state/StatefulPageWrapper.tsx delete mode 100644 apps/website/hooks/dashboard/useDashboardOverview.ts delete mode 100644 apps/website/hooks/driver/useDriverLeaderboard.ts create mode 100644 apps/website/hooks/driver/useDriverProfilePageData.ts delete mode 100644 apps/website/hooks/league/useAllLeaguesWithSponsors.ts delete mode 100644 apps/website/hooks/league/useLeagueDetailWithSponsors.ts create mode 100644 apps/website/hooks/league/useLeagueScheduleAdminPageData.ts create mode 100644 apps/website/hooks/league/useLeagueSponsorshipsPageData.ts create mode 100644 apps/website/hooks/league/useLeagueStewardingMutations.ts delete mode 100644 apps/website/hooks/league/useLeagueWallet.ts create mode 100644 apps/website/hooks/league/useLeagueWalletPageData.ts delete mode 100644 apps/website/hooks/protest/useLeagueProtests.ts create mode 100644 apps/website/hooks/race/useAllRacesPageData.ts delete mode 100644 apps/website/hooks/race/useCancelRace.ts delete mode 100644 apps/website/hooks/race/useCompleteRace.ts delete mode 100644 apps/website/hooks/race/useRaceDetail.ts delete mode 100644 apps/website/hooks/race/useRaceResultsDetail.ts create mode 100644 apps/website/hooks/race/useRaceResultsPageData.ts delete mode 100644 apps/website/hooks/race/useRaceStewardingData.ts delete mode 100644 apps/website/hooks/race/useRaceWithSOF.ts delete mode 100644 apps/website/hooks/race/useRacesPageData.ts delete mode 100644 apps/website/hooks/race/useReopenRace.ts create mode 100644 apps/website/hooks/sponsor/useSponsorshipRequestsPageData.ts delete mode 100644 apps/website/hooks/useLeagueMembershipService.ts rename apps/website/hooks/{penalty/useRacePenalties.ts => usePenaltyTypesReference.ts} (69%) delete mode 100644 apps/website/lib/di/MIGRATION_SUMMARY.md delete mode 100644 apps/website/lib/di/README.md delete mode 100644 apps/website/lib/hooks/useLeagueStewarding.ts delete mode 100644 apps/website/lib/hooks/useRaceResults.ts delete mode 100644 apps/website/lib/hooks/useRaceStewarding.ts delete mode 100644 apps/website/lib/hooks/useTeamLeaderboard.ts create mode 100644 apps/website/lib/page/PageDataFetcher.ts create mode 100644 apps/website/lib/page/usePageData.ts create mode 100644 apps/website/lib/services/home/getHomeData.ts create mode 100644 apps/website/lib/transformers/RaceResultsDataTransformer.ts create mode 100644 apps/website/lib/utils.ts create mode 100644 apps/website/templates/DashboardTemplate.tsx create mode 100644 apps/website/templates/HomeTemplate.tsx create mode 100644 apps/website/templates/LeagueAdminScheduleTemplate.tsx create mode 100644 apps/website/templates/SponsorLeagueDetailTemplate.tsx rename apps/website/{app/sponsor/leagues/SponsorLeaguesInteractive.tsx => templates/SponsorLeaguesTemplate.tsx} (91%) create mode 100644 apps/website/templates/SponsorshipRequestsTemplate.tsx diff --git a/apps/website/AUTH_FIXES_SUMMARY.md b/apps/website/AUTH_FIXES_SUMMARY.md deleted file mode 100644 index 5794489b9..000000000 --- a/apps/website/AUTH_FIXES_SUMMARY.md +++ /dev/null @@ -1,203 +0,0 @@ -# Authentication Loading State Fixes - -## Problem -Users were experiencing an infinite "loading" state when accessing protected routes like `/dashboard`. The page would show "Loading..." indefinitely instead of either displaying the content or redirecting to login. - -## Root Cause Analysis - -The issue was caused by a mismatch between multiple authentication state management systems: - -1. **AuthContext**: Managed session state and loading flag -2. **AuthorizationBlocker**: Determined access reasons based on session state -3. **AuthGateway**: Combined context and blocker state -4. **RouteGuard**: Handled UI rendering and redirects - -### The Problem Flow: -``` -1. User visits /dashboard -2. AuthContext initializes: session = null, loading = false -3. AuthGuard checks access -4. AuthorizationBlocker sees session = null → returns 'loading' -5. AuthGateway sees blocker.reason = 'loading' → sets isLoading = true -6. RouteGuard shows loading state -7. Session fetch completes: session = null, loading = false -8. But blocker still returns 'loading' because session is null -9. Infinite loading state -``` - -## Fixes Applied - -### 1. AuthContext.tsx -**Problem**: Initial loading state was `false`, but session fetch wasn't tracked -**Fix**: -```typescript -// Before -const [loading, setLoading] = useState(false); -const fetchSession = useCallback(async () => { - try { - const current = await sessionService.getSession(); - setSession(current); - } catch { - setSession(null); - } -}, [sessionService]); - -// After -const [loading, setLoading] = useState(true); // Start with loading = true -const fetchSession = useCallback(async () => { - setLoading(true); // Set loading when starting fetch - try { - const current = await sessionService.getSession(); - setSession(current); - } catch { - setSession(null); - } finally { - setLoading(false); // Clear loading when done - } -}, [sessionService]); -``` - -### 2. AuthGateway.ts -**Problem**: Was checking both `authContext.loading` AND `blocker.reason === 'loading'` -**Fix**: Only check authContext.loading for the isLoading state -```typescript -// Before -isLoading: this.authContext.loading || reason === 'loading', - -// After -isLoading: this.authContext.loading, -``` - -### 3. AuthorizationBlocker.ts -**Problem**: Returned 'loading' when session was null, creating confusion -**Fix**: Treat null session as unauthenticated, not loading -```typescript -// Before -getReason(): AuthorizationBlockReason { - if (!this.currentSession) { - return 'loading'; - } - // ... -} - -// After -getReason(): AuthorizationBlockReason { - if (!this.currentSession) { - return 'unauthenticated'; // Null = unauthenticated - } - // ... -} - -canExecute(): boolean { - const reason = this.getReason(); - return reason === 'enabled'; // Only enabled grants access -} -``` - -### 4. RouteGuard.tsx -**Problem**: Generic loading message, unclear redirect flow -**Fix**: Better user feedback during authentication flow -```typescript -// Loading state shows verification message -if (accessState.isLoading) { - return loadingComponent || ( -
- -
- ); -} - -// Unauthorized shows redirect message before redirecting -if (!accessState.canAccess && config.redirectOnUnauthorized !== false) { - return ( -
- -
- ); -} -``` - -### 5. Dashboard Page -**Problem**: Had redundant auth checks that conflicted with layout protection -**Fix**: Simplified to only handle data loading -```typescript -// Before: Had auth checks, useEffect for redirects, etc. -export default function DashboardPage() { - const router = useRouter(); - const { session, loading: authLoading } = useAuth(); - // ... complex auth logic - -// After: Only handles data loading -export default function DashboardPage() { - const { data: dashboardData, isLoading, error } = useDashboardOverview(); - // ... simple data loading -} -``` - -## New Authentication Flow - -### Unauthenticated User: -1. User visits `/dashboard` -2. Middleware checks for `gp_session` cookie → not found -3. Middleware redirects to `/auth/login?returnTo=/dashboard` -4. User logs in -5. Session created, cookie set -6. Redirected back to `/dashboard` -7. AuthGuard verifies session exists -8. Dashboard loads - -### Authenticated User: -1. User visits `/dashboard` -2. Middleware checks for `gp_session` cookie → found -3. Request proceeds to page rendering -4. AuthGuard shows "Verifying authentication..." (briefly) -5. Session verified via AuthContext -6. AuthGuard shows "Redirecting to login..." (if unauthorized) -7. Or renders dashboard content - -### Loading State Resolution: -``` -Initial: session=null, loading=true → AuthGuard shows "Verifying..." -Fetch completes: session=null, loading=false → AuthGuard redirects to login -``` - -## Files Modified - -1. `apps/website/lib/auth/AuthContext.tsx` - Fixed loading state management -2. `apps/website/lib/gateways/AuthGateway.ts` - Simplified isLoading logic -3. `apps/website/lib/blockers/AuthorizationBlocker.ts` - Removed 'loading' reason -4. `apps/website/lib/gateways/RouteGuard.tsx` - Improved user feedback -5. `apps/website/app/dashboard/page.tsx` - Removed redundant auth checks -6. `apps/website/app/dashboard/layout.tsx` - Added AuthGuard protection -7. `apps/website/app/profile/layout.tsx` - Added AuthGuard protection -8. `apps/website/app/sponsor/layout.tsx` - Added AuthGuard protection -9. `apps/website/app/onboarding/layout.tsx` - Added AuthGuard protection -10. `apps/website/app/admin/layout.tsx` - Added RouteGuard protection - -## Testing the Fix - -### Expected Behavior: -- **Unauthenticated access**: Redirects to login within 500ms -- **Authenticated access**: Shows dashboard after brief verification -- **No infinite loading**: Loading states resolve properly - -### Test Scenarios: -1. Clear cookies, visit `/dashboard` → Should redirect to login -2. Login, visit `/dashboard` → Should show dashboard -3. Login, clear cookies, refresh → Should redirect to login -4. Login as non-admin, visit `/admin` → Should redirect to login - -## Security Notes - -- **Defense in depth**: Multiple protection layers (middleware + layout + page) -- **No security bypass**: All fixes maintain security requirements -- **User experience**: Clear feedback during authentication flow -- **Performance**: Minimal overhead, only necessary checks - -## Future Improvements - -1. Add role-based access to SessionViewModel -2. Implement proper backend role system -3. Add session refresh mechanism -4. Implement proper token validation -5. Add authentication state persistence \ No newline at end of file diff --git a/apps/website/CLEAN_ARCHITECTURE_PLAN.md b/apps/website/CLEAN_ARCHITECTURE_PLAN.md new file mode 100644 index 000000000..89ae3812e --- /dev/null +++ b/apps/website/CLEAN_ARCHITECTURE_PLAN.md @@ -0,0 +1,1003 @@ +# Clean Architecture Plan: Unified Data Fetching with SOLID OOP + +## Executive Summary +This plan eliminates file proliferation and establishes a unified, type-safe data fetching architecture that handles **all** real-world scenarios: SSR, CSR, complex state, mutations, and multi-service dependencies. + +## Core Principles + +### 1. Single Responsibility +- **Data Fetching**: `PageDataFetcher` (SSR) + `usePageData` (CSR) +- **State Management**: `PageWrapper` handles loading/error/empty states +- **Business Logic**: Service classes handle domain logic +- **UI Rendering**: Templates handle presentation + +### 2. Open/Closed Principle +- Extend via composition, not modification +- Add new service methods without changing fetchers +- Support new patterns via strategy pattern + +### 3. Dependency Inversion +- High-level modules depend on abstractions +- Use DI container for SSR +- Use hooks for CSR + +--- + +## Architecture Components + +### 1. Unified Data Fetcher (SSR) - UPDATED FOR REALITY +**File**: `lib/page/PageDataFetcher.ts` + +```typescript +import { ContainerManager } from '@/lib/di/container'; + +export interface FetchResult { + data: T | null; + errors: Record; + hasErrors: boolean; +} + +export class PageDataFetcher { + /** + * Fetch data using DI container + * Use for: Simple SSR pages with single service + * WARNING: Container is singleton - avoid stateful services + */ + static async fetch( + ServiceToken: string | symbol, + method: TMethod, + ...args: TService[TMethod] extends (...params: infer P) => Promise ? P : never + ): Promise<(TService[TMethod] extends (...params: any[]) => Promise ? R : never) | null> { + try { + const container = ContainerManager.getInstance().getContainer(); + const service = container.get(ServiceToken); + const result = await (service[method] as Function)(...args); + return result; + } catch (error) { + console.error(`Failed to fetch: ${String(ServiceToken)}.${String(method)}`, error); + return null; + } + } + + /** + * Fetch using manual service instantiation + * Use for: Multiple dependencies, request-scoped services, or auth context + * RECOMMENDED for SSR over fetch() with DI + */ + static async fetchManual( + serviceFactory: () => Promise | TData + ): Promise { + try { + const result = await serviceFactory(); + return result; + } catch (error) { + console.error('Failed to fetch manual:', error); + return null; + } + } + + /** + * Fetch multiple datasets in parallel with error aggregation + * Use for: Pages needing multiple service calls + * UPDATED: Returns both data and errors for proper handling + */ + static async fetchMultiple>( + queries: T + ): Promise> { + const results = {} as { [K in keyof T]: T[K] }; + const errors = {} as Record; + + const entries = await Promise.all( + Object.entries(queries).map(async ([key, query]) => { + try { + const result = await query(); + return [key, { success: true, data: result }]; + } catch (error) { + console.error(`Failed to fetch ${key}:`, error); + return [key, { success: false, error: error instanceof Error ? error : new Error(String(error)) }]; + } + }) + ); + + entries.forEach(([key, result]) => { + if (result.success) { + results[key as keyof T] = result.data; + } else { + errors[key] = result.error; + } + }); + + return { + data: results, + errors, + hasErrors: Object.keys(errors).length > 0 + }; + } +} +``` + +### 2. Client-Side Data Hook - UPDATED FOR REALITY +**File**: `lib/page/usePageData.ts` + +```typescript +'use client'; + +import { useQuery, useQueries, UseQueryOptions, useMutation, UseMutationOptions } from '@tanstack/react-query'; +import { useInject } from '@/lib/di/hooks/useInject'; +import { ApiError } from '@/lib/api/base/ApiError'; + +export interface PageDataConfig { + queryKey: string[]; + queryFn: () => Promise; + enabled?: boolean; + staleTime?: number; + onError?: (error: TError) => void; +} + +/** + * Single query hook - STANDARDIZED PATTERN + * Use for: Simple CSR pages + * + * @example + * const { data, isLoading, error, refetch } = usePageData({ + * queryKey: ['profile'], + * queryFn: () => driverService.getProfile(), + * }); + */ +export function usePageData( + config: PageDataConfig +) { + return useQuery({ + queryKey: config.queryKey, + queryFn: config.queryFn, + enabled: config.enabled ?? true, + staleTime: config.staleTime ?? 1000 * 60 * 5, + onError: config.onError, + }); +} + +/** + * Multiple queries hook - STANDARDIZED PATTERN + * Use for: Complex CSR pages with multiple data sources + * + * @example + * const { data, isLoading, error, refetch } = usePageDataMultiple({ + * results: { + * queryKey: ['raceResults', raceId], + * queryFn: () => service.getResults(raceId), + * }, + * sof: { + * queryKey: ['raceSOF', raceId], + * queryFn: () => service.getSOF(raceId), + * }, + * }); + */ +export function usePageDataMultiple>( + queries: { + [K in keyof T]: PageDataConfig; + } +) { + const queryResults = useQueries({ + queries: Object.entries(queries).map(([key, config]) => ({ + queryKey: config.queryKey, + queryFn: config.queryFn, + enabled: config.enabled ?? true, + staleTime: config.staleTime ?? 1000 * 60 * 5, + onError: config.onError, + })), + }); + + // Combine results + const combined = {} as { [K in keyof T]: T[K] | null }; + const keys = Object.keys(queries) as (keyof T)[]; + + keys.forEach((key, index) => { + combined[key] = queryResults[index].data ?? null; + }); + + const isLoading = queryResults.some(q => q.isLoading); + const error = queryResults.find(q => q.error)?.error ?? null; + + return { + data: combined, + isLoading, + error, + refetch: () => queryResults.forEach(q => q.refetch()), + }; +} + +/** + * Mutation hook wrapper - STANDARDIZED PATTERN + * Use for: All mutation operations + * + * @example + * const mutation = usePageMutation( + * (variables) => service.mutateData(variables), + * { onSuccess: () => refetch() } + * ); + */ +export function usePageMutation( + mutationFn: (variables: TVariables) => Promise, + options?: Omit, 'mutationFn'> +) { + return useMutation({ + mutationFn, + ...options, + }); +} + +/** + * SSR Hydration Hook - NEW + * Use for: Passing SSR data to CSR to avoid re-fetching + * + * @example + * // In SSR page + * const ssrData = await PageDataFetcher.fetch(...); + * + * // In client component + * const { data } = useHydrateSSRData(ssrData, ['queryKey']); + */ +export function useHydrateSSRData( + ssrData: TData | null, + queryKey: string[] +): { data: TData | null; isHydrated: boolean } { + const [isHydrated, setIsHydrated] = React.useState(false); + + React.useEffect(() => { + if (ssrData !== null) { + setIsHydrated(true); + } + }, [ssrData]); + + return { + data: ssrData, + isHydrated, + }; +} +``` + +### 3. Universal Page Wrapper - UPDATED FOR SSR/CSR COMPATIBILITY +**File**: `components/shared/state/PageWrapper.tsx` + +```typescript +import React from 'react'; +import { ApiError } from '@/lib/api/base/ApiError'; +import { LoadingWrapper } from './LoadingWrapper'; +import { ErrorDisplay } from './ErrorDisplay'; +import { EmptyState } from './EmptyState'; + +export interface PageWrapperLoadingConfig { + variant?: 'skeleton' | 'full-screen'; + message?: string; +} + +export interface PageWrapperErrorConfig { + variant?: 'full-screen' | 'card'; + card?: { + title?: string; + description?: string; + }; +} + +export interface PageWrapperEmptyConfig { + icon?: React.ElementType; + title?: string; + description?: string; + action?: { + label: string; + onClick: () => void; + }; +} + +export interface PageWrapperProps { + /** Data to be rendered */ + data: TData | undefined; + /** Loading state (default: false) */ + isLoading?: boolean; + /** Error state (default: null) */ + error?: Error | null; + /** Retry function for errors */ + retry?: () => void; + /** Template component that receives the data */ + Template: React.ComponentType<{ data: TData }>; + /** Loading configuration */ + loading?: PageWrapperLoadingConfig; + /** Error configuration */ + errorConfig?: PageWrapperErrorConfig; + /** Empty configuration */ + empty?: PageWrapperEmptyConfig; + /** Children for flexible content rendering */ + children?: React.ReactNode; + /** Additional CSS classes */ + className?: string; +} + +/** + * PageWrapper Component - SSR/CSR COMPATIBLE + * + * CRITICAL: This component is NOT marked 'use client' to work in SSR pages + * For CSR pages, use the wrapper version below + * + * Usage in SSR: + * ```typescript + * export default async function Page() { + * const data = await PageDataFetcher.fetch(...); + * return ; + * } + * ``` + * + * Usage in CSR: + * ```typescript + * export default function Page() { + * const { data, isLoading, error } = usePageData(...); + * return ( + * + * ); + * } + * ``` + */ +export function PageWrapper({ + data, + isLoading = false, + error = null, + retry, + Template, + loading, + errorConfig, + empty, + children, + className = '', +}: PageWrapperProps) { + // Priority order: Loading > Error > Empty > Success + + // 1. Loading State + if (isLoading) { + const loadingVariant = loading?.variant || 'skeleton'; + const loadingMessage = loading?.message || 'Loading...'; + + if (loadingVariant === 'full-screen') { + return ( + + ); + } + + // Default to skeleton + return ( +
+ + {children} +
+ ); + } + + // 2. Error State + if (error) { + const errorVariant = errorConfig?.variant || 'full-screen'; + + if (errorVariant === 'card') { + const cardTitle = errorConfig?.card?.title || 'Error'; + const cardDescription = errorConfig?.card?.description || 'Something went wrong'; + + return ( +
+ + {children} +
+ ); + } + + // Default to full-screen + return ( + + ); + } + + // 3. Empty State + if (!data || (Array.isArray(data) && data.length === 0)) { + if (empty) { + const Icon = empty.icon; + const hasAction = empty.action && retry; + + return ( +
+ + {children} +
+ ); + } + + // If no empty config provided but data is empty, show nothing + return ( +
+ {children} +
+ ); + } + + // 4. Success State - Render Template with data + return ( +
+