fix issues

This commit is contained in:
2026-01-07 16:20:19 +01:00
parent 3b3971e653
commit 1b63fa646c
17 changed files with 758 additions and 187 deletions

View File

@@ -19,7 +19,7 @@ const DEFAULT_CONFIG_PATH = 'apps/api/src/config/features.config.ts';
* Output: { 'sponsors.portal': 'enabled' }
*/
function flattenFeatures(
config: Record<string, any>,
config: Record<string, unknown>,
prefix: string = ''
): FlattenedFeatures {
const flattened: FlattenedFeatures = {};
@@ -29,7 +29,7 @@ function flattenFeatures(
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Recursively flatten nested objects
Object.assign(flattened, flattenFeatures(value, fullKey));
Object.assign(flattened, flattenFeatures(value as Record<string, unknown>, fullKey));
} else if (isFeatureState(value)) {
// Assign feature state
flattened[fullKey] = value;

View File

@@ -6,6 +6,7 @@ import { AuthProvider } from '@/lib/auth/AuthContext';
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
import { FeatureFlagProvider } from '@/lib/feature/FeatureFlagProvider';
import { ContainerProvider } from '@/lib/di/providers/ContainerProvider';
import { QueryClientProvider } from '@/lib/providers/QueryClientProvider';
import { initializeGlobalErrorHandling } from '@/lib/infrastructure/GlobalErrorHandler';
import { initializeApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
import { Metadata, Viewport } from 'next';
@@ -81,39 +82,41 @@ export default async function RootLayout({
</head>
<body className="antialiased overflow-x-hidden">
<ContainerProvider>
<AuthProvider>
<FeatureFlagProvider flags={enabledFlags}>
<NotificationProvider>
<NotificationIntegration />
<EnhancedErrorBoundary enableDevOverlay={process.env.NODE_ENV === 'development'}>
<header className="fixed top-0 left-0 right-0 z-50 bg-deep-graphite/80 backdrop-blur-sm border-b border-white/5">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Link href="/" className="inline-flex items-center">
<Image
src="/images/logos/wordmark-rectangle-dark.svg"
alt="GridPilot"
width={160}
height={30}
className="h-6 w-auto md:h-8"
priority
/>
</Link>
<p className="hidden sm:block text-sm text-gray-400 font-light">
Making league racing less chaotic
</p>
<QueryClientProvider>
<AuthProvider>
<FeatureFlagProvider flags={enabledFlags}>
<NotificationProvider>
<NotificationIntegration />
<EnhancedErrorBoundary enableDevOverlay={process.env.NODE_ENV === 'development'}>
<header className="fixed top-0 left-0 right-0 z-50 bg-deep-graphite/80 backdrop-blur-sm border-b border-white/5">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Link href="/" className="inline-flex items-center">
<Image
src="/images/logos/wordmark-rectangle-dark.svg"
alt="GridPilot"
width={160}
height={30}
className="h-6 w-auto md:h-8"
priority
/>
</Link>
<p className="hidden sm:block text-sm text-gray-400 font-light">
Making league racing less chaotic
</p>
</div>
</div>
</div>
</div>
</header>
<div className="pt-16">{children}</div>
{/* Development Tools */}
{process.env.NODE_ENV === 'development' && <DevToolbar />}
</EnhancedErrorBoundary>
</NotificationProvider>
</FeatureFlagProvider>
</AuthProvider>
</header>
<div className="pt-16">{children}</div>
{/* Development Tools */}
{process.env.NODE_ENV === 'development' && <DevToolbar />}
</EnhancedErrorBoundary>
</NotificationProvider>
</FeatureFlagProvider>
</AuthProvider>
</QueryClientProvider>
</ContainerProvider>
</body>
</html>

View File

@@ -0,0 +1,49 @@
'use client';
import { useRouter } from 'next/navigation';
import LeaderboardsTemplate from '@/templates/LeaderboardsTemplate';
import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
interface LeaderboardsPageData {
drivers: DriverLeaderboardViewModel | null;
teams: TeamSummaryViewModel[] | null;
}
export function LeaderboardsPageWrapper({ data }: { data: LeaderboardsPageData | null }) {
const router = useRouter();
if (!data || (!data.drivers && !data.teams)) {
return null;
}
const drivers = data.drivers?.drivers || [];
const teams = data.teams || [];
const handleDriverClick = (driverId: string) => {
router.push(`/drivers/${driverId}`);
};
const handleTeamClick = (teamId: string) => {
router.push(`/teams/${teamId}`);
};
const handleNavigateToDrivers = () => {
router.push('/leaderboards/drivers');
};
const handleNavigateToTeams = () => {
router.push('/teams/leaderboard');
};
return (
<LeaderboardsTemplate
drivers={drivers}
teams={teams}
onDriverClick={handleDriverClick}
onTeamClick={handleTeamClick}
onNavigateToDrivers={handleNavigateToDrivers}
onNavigateToTeams={handleNavigateToTeams}
/>
);
}

View File

@@ -0,0 +1,48 @@
'use client';
import { useRouter } from 'next/navigation';
import DriverRankingsTemplate from '@/templates/DriverRankingsTemplate';
import { useState } from 'react';
import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
export function DriverRankingsPageWrapper({ data }: { data: DriverLeaderboardViewModel | null }) {
const router = useRouter();
// Client-side state for filtering and sorting
const [searchQuery, setSearchQuery] = useState('');
const [selectedSkill, setSelectedSkill] = useState<'all' | SkillLevel>('all');
const [sortBy, setSortBy] = useState<SortBy>('rank');
const [showFilters, setShowFilters] = useState(false);
if (!data || !data.drivers) {
return null;
}
const handleDriverClick = (driverId: string) => {
if (driverId.startsWith('demo-')) return;
router.push(`/drivers/${driverId}`);
};
const handleBackToLeaderboards = () => {
router.push('/leaderboards');
};
return (
<DriverRankingsTemplate
drivers={data.drivers}
searchQuery={searchQuery}
selectedSkill={selectedSkill}
sortBy={sortBy}
showFilters={showFilters}
onSearchChange={setSearchQuery}
onSkillChange={setSelectedSkill}
onSortChange={setSortBy}
onToggleFilters={() => setShowFilters(!showFilters)}
onDriverClick={handleDriverClick}
onBackToLeaderboards={handleBackToLeaderboards}
/>
);
}

View File

@@ -1,62 +1,11 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import DriverRankingsTemplate from '@/templates/DriverRankingsTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
import { DriverService } from '@/lib/services/drivers/DriverService';
import { Users } from 'lucide-react';
import { redirect , useRouter } from 'next/navigation';
import { redirect } from 'next/navigation';
import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
import { useState } from 'react';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
// ============================================================================
// WRAPPER COMPONENT (Client-side state management)
// ============================================================================
function DriverRankingsPageWrapper({ data }: { data: DriverLeaderboardViewModel | null }) {
const router = useRouter();
// Client-side state for filtering and sorting
const [searchQuery, setSearchQuery] = useState('');
const [selectedSkill, setSelectedSkill] = useState<'all' | SkillLevel>('all');
const [sortBy, setSortBy] = useState<SortBy>('rank');
const [showFilters, setShowFilters] = useState(false);
if (!data || !data.drivers) {
return null;
}
const handleDriverClick = (driverId: string) => {
if (driverId.startsWith('demo-')) return;
router.push(`/drivers/${driverId}`);
};
const handleBackToLeaderboards = () => {
router.push('/leaderboards');
};
return (
<DriverRankingsTemplate
drivers={data.drivers}
searchQuery={searchQuery}
selectedSkill={selectedSkill}
sortBy={sortBy}
showFilters={showFilters}
onSearchChange={setSearchQuery}
onSkillChange={setSelectedSkill}
onSortChange={setSortBy}
onToggleFilters={() => setShowFilters(!showFilters)}
onDriverClick={handleDriverClick}
onBackToLeaderboards={handleBackToLeaderboards}
/>
);
}
import { DriverRankingsPageWrapper } from './DriverRankingsPageWrapper';
// ============================================================================
// MAIN PAGE COMPONENT

View File

@@ -1,13 +1,13 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import LeaderboardsTemplate from '@/templates/LeaderboardsTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { DRIVER_SERVICE_TOKEN, TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
import { DriverService } from '@/lib/services/drivers/DriverService';
import { TeamService } from '@/lib/services/teams/TeamService';
import { Trophy } from 'lucide-react';
import { redirect , useRouter } from 'next/navigation';
import { redirect } from 'next/navigation';
import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { LeaderboardsPageWrapper } from './LeaderboardsPageWrapper';
// ============================================================================
// TYPES
@@ -18,48 +18,6 @@ interface LeaderboardsPageData {
teams: TeamSummaryViewModel[] | null;
}
// ============================================================================
// WRAPPER COMPONENT
// ============================================================================
function LeaderboardsPageWrapper({ data }: { data: LeaderboardsPageData | null }) {
const router = useRouter();
if (!data || (!data.drivers && !data.teams)) {
return null;
}
const drivers = data.drivers?.drivers || [];
const teams = data.teams || [];
const handleDriverClick = (driverId: string) => {
router.push(`/drivers/${driverId}`);
};
const handleTeamClick = (teamId: string) => {
router.push(`/teams/${teamId}`);
};
const handleNavigateToDrivers = () => {
router.push('/leaderboards/drivers');
};
const handleNavigateToTeams = () => {
router.push('/teams/leaderboard');
};
return (
<LeaderboardsTemplate
drivers={drivers}
teams={teams}
onDriverClick={handleDriverClick}
onTeamClick={handleTeamClick}
onNavigateToDrivers={handleNavigateToDrivers}
onNavigateToTeams={handleNavigateToTeams}
/>
);
}
// ============================================================================
// MAIN PAGE COMPONENT
// ============================================================================

View File

@@ -0,0 +1,44 @@
'use client';
import { useRouter } from 'next/navigation';
import TeamLeaderboardTemplate from '@/templates/TeamLeaderboardTemplate';
import { useState } from 'react';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
export function TeamLeaderboardPageWrapper({ data }: { data: TeamSummaryViewModel[] | null }) {
const router = useRouter();
// Client-side state for filtering and sorting
const [searchQuery, setSearchQuery] = useState('');
const [filterLevel, setFilterLevel] = useState<SkillLevel | 'all'>('all');
const [sortBy, setSortBy] = useState<SortBy>('rating');
if (!data || data.length === 0) {
return null;
}
const handleTeamClick = (teamId: string) => {
router.push(`/teams/${teamId}`);
};
const handleBackToTeams = () => {
router.push('/teams');
};
return (
<TeamLeaderboardTemplate
teams={data}
searchQuery={searchQuery}
filterLevel={filterLevel}
sortBy={sortBy}
onSearchChange={setSearchQuery}
onFilterLevelChange={setFilterLevel}
onSortChange={setSortBy}
onTeamClick={handleTeamClick}
onBackToTeams={handleBackToTeams}
/>
);
}

View File

@@ -1,58 +1,11 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import TeamLeaderboardTemplate from '@/templates/TeamLeaderboardTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
import { TeamService } from '@/lib/services/teams/TeamService';
import { Trophy } from 'lucide-react';
import { redirect , useRouter } from 'next/navigation';
import { redirect } from 'next/navigation';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { useState } from 'react';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
// ============================================================================
// WRAPPER COMPONENT (Client-side state management)
// ============================================================================
function TeamLeaderboardPageWrapper({ data }: { data: TeamSummaryViewModel[] | null }) {
const router = useRouter();
// Client-side state for filtering and sorting
const [searchQuery, setSearchQuery] = useState('');
const [filterLevel, setFilterLevel] = useState<SkillLevel | 'all'>('all');
const [sortBy, setSortBy] = useState<SortBy>('rating');
if (!data || data.length === 0) {
return null;
}
const handleTeamClick = (teamId: string) => {
router.push(`/teams/${teamId}`);
};
const handleBackToTeams = () => {
router.push('/teams');
};
return (
<TeamLeaderboardTemplate
teams={data}
searchQuery={searchQuery}
filterLevel={filterLevel}
sortBy={sortBy}
onSearchChange={setSearchQuery}
onFilterLevelChange={setFilterLevel}
onSortChange={setSortBy}
onTeamClick={handleTeamClick}
onBackToTeams={handleBackToTeams}
/>
);
}
import { TeamLeaderboardPageWrapper } from './TeamLeaderboardPageWrapper';
// ============================================================================
// MAIN PAGE COMPONENT

View File

@@ -2,6 +2,7 @@ import { Container } from 'inversify';
// Module imports
import { ApiModule } from './modules/api.module';
import { AuthModule } from './modules/auth.module';
import { CoreModule } from './modules/core.module';
import { DashboardModule } from './modules/dashboard.module';
import { DriverModule } from './modules/driver.module';
@@ -23,6 +24,7 @@ export function createContainer(): Container {
container.load(
CoreModule,
ApiModule,
AuthModule,
LeagueModule,
DriverModule,
TeamModule,

View File

@@ -13,6 +13,7 @@ export * from './providers/ContainerProvider';
// Modules
export * from './modules/analytics.module';
export * from './modules/api.module';
export * from './modules/auth.module';
export * from './modules/core.module';
export * from './modules/dashboard.module';
export * from './modules/driver.module';

View File

@@ -0,0 +1,30 @@
import { ContainerModule } from 'inversify';
import { AuthService } from '../../services/auth/AuthService';
import { SessionService } from '../../services/auth/SessionService';
import { AuthApiClient } from '../../api/auth/AuthApiClient';
import {
AUTH_SERVICE_TOKEN,
SESSION_SERVICE_TOKEN,
AUTH_API_CLIENT_TOKEN
} from '../tokens';
export const AuthModule = new ContainerModule((options) => {
const bind = options.bind;
// Session Service
bind<SessionService>(SESSION_SERVICE_TOKEN)
.toDynamicValue((ctx) => {
const authApiClient = ctx.get<AuthApiClient>(AUTH_API_CLIENT_TOKEN);
return new SessionService(authApiClient);
})
.inSingletonScope();
// Auth Service
bind<AuthService>(AUTH_SERVICE_TOKEN)
.toDynamicValue((ctx) => {
const authApiClient = ctx.get<AuthApiClient>(AUTH_API_CLIENT_TOKEN);
return new AuthService(authApiClient);
})
.inSingletonScope();
});

View File

@@ -91,6 +91,9 @@ export const LeagueModule = new ContainerModule((options) => {
// League Membership Service
bind<LeagueMembershipService>(LEAGUE_MEMBERSHIP_SERVICE_TOKEN)
.to(LeagueMembershipService)
.toDynamicValue((ctx) => {
const leagueApiClient = ctx.get<LeaguesApiClient>(LEAGUE_API_CLIENT_TOKEN);
return new LeagueMembershipService(leagueApiClient);
})
.inSingletonScope();
});

View File

@@ -35,14 +35,29 @@ export class ConsoleLogger implements Logger {
const emoji = this.EMOJIS[level];
const prefix = this.PREFIXES[level];
console.groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`);
// Edge runtime doesn't support console.groupCollapsed/groupEnd
// Fallback to simple logging for compatibility
const supportsGrouping = typeof console.groupCollapsed === 'function' && typeof console.groupEnd === 'function';
if (supportsGrouping) {
// Safe to call - we've verified both functions exist
(console as any).groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`);
} else {
// Simple format for edge runtime
console.log(`${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`);
}
console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', new Date().toISOString());
console.log(`%cSource:`, 'color: #666; font-weight: bold;', source);
if (context) {
console.log(`%cContext:`, 'color: #666; font-weight: bold;');
console.dir(context, { depth: 3, colors: true });
// console.dir may not be available in edge runtime
if (typeof console.dir === 'function') {
console.dir(context, { depth: 3, colors: true });
} else {
console.log(JSON.stringify(context, null, 2));
}
}
if (error) {
@@ -56,7 +71,10 @@ export class ConsoleLogger implements Logger {
}
}
console.groupEnd();
if (supportsGrouping) {
// Safe to call - we've verified the function exists
(console as any).groupEnd();
}
}
debug(message: string, context?: unknown): void {

View File

@@ -0,0 +1,42 @@
'use client';
import { QueryClient, QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
import { ReactNode, useState } from 'react';
interface QueryClientProviderProps {
children: ReactNode;
}
/**
* Provides React Query client to the application
*
* Must wrap any components that use React Query hooks (useQuery, useMutation, etc.)
* Creates a new QueryClient instance per component tree to avoid state sharing
*/
export function QueryClientProvider({ children }: QueryClientProviderProps) {
// Create a new QueryClient instance for each component tree
// This prevents state sharing between different renders
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// Disable automatic refetching in production for better performance
refetchOnWindowFocus: process.env.NODE_ENV === 'development',
refetchOnReconnect: true,
retry: 1,
staleTime: 5 * 60 * 1000, // 5 minutes
},
mutations: {
retry: 0,
},
},
})
);
return (
<TanstackQueryClientProvider client={queryClient}>
{children}
</TanstackQueryClientProvider>
);
}