page wrapper

This commit is contained in:
2026-01-07 12:40:52 +01:00
parent e589c30bf8
commit 0db80fa98d
128 changed files with 7386 additions and 8096 deletions

View File

@@ -1,98 +0,0 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import { LeagueStandingsTemplate } from '@/templates/LeagueStandingsTemplate';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
export default function LeagueStandingsInteractive() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
const [standings, setStandings] = useState<StandingEntryViewModel[]>([]);
const [drivers, setDrivers] = useState<DriverViewModel[]>([]);
const [memberships, setMemberships] = useState<LeagueMembership[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
const loadData = useCallback(async () => {
try {
const vm = await leagueService.getLeagueStandings(leagueId, currentDriverId);
setStandings(vm.standings);
setDrivers(vm.drivers.map((d) => new DriverViewModel({ ...d, avatarUrl: (d as any).avatarUrl ?? null })));
setMemberships(vm.memberships);
// Check if current user is admin
const membership = vm.memberships.find(m => m.driverId === currentDriverId);
setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load standings');
} finally {
setLoading(false);
}
}, [leagueId, currentDriverId, leagueService]);
useEffect(() => {
loadData();
}, [loadData]);
const handleRemoveMember = async (driverId: string) => {
if (!confirm('Are you sure you want to remove this member?')) {
return;
}
try {
await leagueService.removeMember(leagueId, currentDriverId, driverId);
await loadData();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to remove member');
}
};
const handleUpdateRole = async (driverId: string, newRole: string) => {
try {
await leagueService.updateMemberRole(leagueId, currentDriverId, driverId, newRole);
await loadData();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update role');
}
};
if (loading) {
return (
<div className="text-center text-gray-400">
Loading standings...
</div>
);
}
if (error) {
return (
<div className="text-center text-warning-amber">
{error}
</div>
);
}
return (
<LeagueStandingsTemplate
standings={standings}
drivers={drivers}
memberships={memberships}
leagueId={leagueId}
currentDriverId={currentDriverId}
isAdmin={isAdmin}
onRemoveMember={handleRemoveMember}
onUpdateRole={handleUpdateRole}
/>
);
}

View File

@@ -1,71 +0,0 @@
import { LeagueStandingsTemplate } from '@/templates/LeagueStandingsTemplate';
import { ServiceFactory } from '@/lib/services/ServiceFactory';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
interface LeagueStandingsStaticProps {
leagueId: string;
currentDriverId?: string | null;
}
export default async function LeagueStandingsStatic({ leagueId, currentDriverId }: LeagueStandingsStaticProps) {
const serviceFactory = new ServiceFactory(getWebsiteApiBaseUrl());
const leagueService = serviceFactory.createLeagueService();
let standings: StandingEntryViewModel[] = [];
let drivers: DriverViewModel[] = [];
let memberships: LeagueMembership[] = [];
let loading = false;
let error: string | null = null;
let isAdmin = false;
try {
loading = true;
const vm = await leagueService.getLeagueStandings(leagueId, currentDriverId || '');
standings = vm.standings;
drivers = vm.drivers.map((d) => new DriverViewModel({ ...d, avatarUrl: (d as any).avatarUrl ?? null }));
memberships = vm.memberships;
// Check if current user is admin
const membership = vm.memberships.find(m => m.driverId === currentDriverId);
isAdmin = membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load standings';
} finally {
loading = false;
}
if (loading) {
return (
<div className="text-center text-gray-400">
Loading standings...
</div>
);
}
if (error) {
return (
<div className="text-center text-warning-amber">
{error}
</div>
);
}
// Server components can't have event handlers, so we provide empty functions
// The Interactive wrapper will add the actual handlers
return (
<LeagueStandingsTemplate
standings={standings}
drivers={drivers}
memberships={memberships}
leagueId={leagueId}
currentDriverId={currentDriverId ?? null}
isAdmin={isAdmin}
onRemoveMember={() => {}}
onUpdateRole={() => {}}
/>
);
}

View File

@@ -1,3 +1,107 @@
import LeagueStandingsInteractive from './LeagueStandingsInteractive';
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { LeagueStandingsTemplate } from '@/templates/LeagueStandingsTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { LeagueService } from '@/lib/services/leagues/LeagueService';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation';
import type { LeagueStandingsViewModel } from '@/lib/view-models/LeagueStandingsViewModel';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
export default LeagueStandingsInteractive;
interface Props {
params: { id: string };
}
export default async function Page({ params }: Props) {
// Validate params
if (!params.id) {
notFound();
}
// Fetch data using PageDataFetcher.fetchManual for multiple dependencies
const data = await PageDataFetcher.fetchManual(async () => {
// Create dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
// Create API clients
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
const driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const sponsorsApiClient = new SponsorsApiClient(baseUrl, errorReporter, logger);
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
// Create service
const service = new LeagueService(
leaguesApiClient,
driversApiClient,
sponsorsApiClient,
racesApiClient
);
// Fetch data - using empty string for currentDriverId since this is SSR without session
const result = await service.getLeagueStandings(params.id, '');
if (!result) {
throw new Error('League standings not found');
}
return result;
});
if (!data) {
notFound();
}
// Transform data for template
const standings = data.standings ?? [];
const drivers: DriverViewModel[] = data.drivers?.map((d) =>
new DriverViewModel({
id: d.id,
name: d.name,
avatarUrl: d.avatarUrl || null,
iracingId: d.iracingId,
rating: d.rating,
country: d.country,
})
) ?? [];
const memberships: LeagueMembership[] = data.memberships ?? [];
// Create a wrapper component that passes data to the template
const TemplateWrapper = () => {
return (
<LeagueStandingsTemplate
standings={standings}
drivers={drivers}
memberships={memberships}
leagueId={params.id}
currentDriverId={null}
isAdmin={false}
onRemoveMember={() => {}}
onUpdateRole={() => {}}
loading={false}
/>
);
};
return (
<PageWrapper
data={data}
Template={TemplateWrapper}
loading={{ variant: 'skeleton', message: 'Loading standings...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
title: 'Standings not found',
description: 'The standings for this league are not available.',
}}
/>
);
}