website refactor
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
import { RaceDetailTemplate } from '@/templates/RaceDetailTemplate';
|
||||
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 { RaceDetailPageQuery } from '@/lib/page-queries/races/RaceDetailPageQuery';
|
||||
|
||||
interface RaceDetailPageProps {
|
||||
params: {
|
||||
@@ -19,72 +16,87 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Manual wiring: 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 client
|
||||
const apiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Fetch initial race data (empty driverId for now, handled client-side)
|
||||
const data = await apiClient.getDetail(raceId, '');
|
||||
// Execute PageQuery
|
||||
const result = await RaceDetailPageQuery.execute({ raceId, driverId: '' });
|
||||
|
||||
if (!data) notFound();
|
||||
|
||||
// Transform data for template
|
||||
const templateViewModel = data && data.race ? {
|
||||
race: {
|
||||
id: data.race.id,
|
||||
track: data.race.track,
|
||||
car: data.race.car,
|
||||
scheduledAt: data.race.scheduledAt,
|
||||
status: data.race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
|
||||
sessionType: data.race.sessionType,
|
||||
},
|
||||
league: data.league ? {
|
||||
id: data.league.id,
|
||||
name: data.league.name,
|
||||
description: data.league.description || undefined,
|
||||
settings: data.league.settings as { maxDrivers: number; qualifyingFormat: string },
|
||||
} : undefined,
|
||||
entryList: data.entryList.map((entry: any) => ({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
avatarUrl: entry.avatarUrl,
|
||||
country: entry.country,
|
||||
rating: entry.rating,
|
||||
isCurrentUser: entry.isCurrentUser,
|
||||
})),
|
||||
registration: {
|
||||
isUserRegistered: data.registration.isUserRegistered,
|
||||
canRegister: data.registration.canRegister,
|
||||
},
|
||||
userResult: data.userResult ? {
|
||||
position: data.userResult.position,
|
||||
startPosition: data.userResult.startPosition,
|
||||
positionChange: data.userResult.positionChange,
|
||||
incidents: data.userResult.incidents,
|
||||
isClean: data.userResult.isClean,
|
||||
isPodium: data.userResult.isPodium,
|
||||
ratingChange: data.userResult.ratingChange,
|
||||
} : undefined,
|
||||
canReopenRace: false, // Not provided by API, default to false
|
||||
} : undefined;
|
||||
if (result.isErr()) {
|
||||
const error = result.getError();
|
||||
|
||||
switch (error) {
|
||||
case 'notFound':
|
||||
notFound();
|
||||
case 'redirect':
|
||||
notFound();
|
||||
default:
|
||||
// Pass error to template via PageWrapper
|
||||
return (
|
||||
<PageWrapper
|
||||
data={null}
|
||||
Template={({ data: _data }) => (
|
||||
<RaceDetailTemplate
|
||||
viewModel={undefined}
|
||||
isLoading={false}
|
||||
error={new Error('Failed to load race details')}
|
||||
onBack={() => {}}
|
||||
onRegister={() => {}}
|
||||
onWithdraw={() => {}}
|
||||
onCancel={() => {}}
|
||||
onReopen={() => {}}
|
||||
onEndRace={() => {}}
|
||||
onFileProtest={() => {}}
|
||||
onResultsClick={() => {}}
|
||||
onStewardingClick={() => {}}
|
||||
onLeagueClick={() => {}}
|
||||
onDriverClick={() => {}}
|
||||
currentDriverId={''}
|
||||
isOwnerOrAdmin={false}
|
||||
showProtestModal={false}
|
||||
setShowProtestModal={() => {}}
|
||||
showEndRaceModal={false}
|
||||
setShowEndRaceModal={() => {}}
|
||||
mutationLoading={{
|
||||
register: false,
|
||||
withdraw: false,
|
||||
cancel: false,
|
||||
reopen: false,
|
||||
complete: false,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
loading={{ variant: 'skeleton', message: 'Loading race details...' }}
|
||||
errorConfig={{ variant: 'full-screen' }}
|
||||
empty={{
|
||||
icon: require('lucide-react').Flag,
|
||||
title: 'Race not found',
|
||||
description: 'The race may have been cancelled or deleted',
|
||||
action: { label: 'Back to Races', onClick: () => {} }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const viewData = result.unwrap();
|
||||
|
||||
// Convert ViewData to ViewModel for the template
|
||||
// The template expects a ViewModel, so we need to adapt
|
||||
const viewModel = {
|
||||
race: viewData.race,
|
||||
league: viewData.league,
|
||||
entryList: viewData.entryList,
|
||||
registration: viewData.registration,
|
||||
userResult: viewData.userResult,
|
||||
canReopenRace: viewData.canReopenRace,
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
data={data}
|
||||
Template={({ data }) => (
|
||||
data={viewData}
|
||||
Template={({ data: _data }) => (
|
||||
<RaceDetailTemplate
|
||||
viewModel={templateViewModel}
|
||||
viewModel={viewModel}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
// These will be handled client-side in the template or a wrapper
|
||||
onBack={() => {}}
|
||||
onRegister={() => {}}
|
||||
onWithdraw={() => {}}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
|
||||
import { RaceResultsTemplate } from '@/templates/RaceResultsTemplate';
|
||||
import { useRaceResultsPageData } from "@/lib/hooks/race/useRaceResultsPageData";
|
||||
import { RaceResultsDataTransformer } from '@/lib/view-models/RaceResultsDataTransformer';
|
||||
import { useLeagueMemberships } from "@/lib/hooks/league/useLeagueMemberships";
|
||||
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
|
||||
import { useState } from 'react';
|
||||
import { notFound, useRouter } from 'next/navigation';
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import { RaceResultsPageQuery } from '@/lib/page-queries/races/RaceResultsPageQuery';
|
||||
import { Trophy } from 'lucide-react';
|
||||
|
||||
interface RaceResultsPageProps {
|
||||
@@ -17,99 +10,101 @@ interface RaceResultsPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default function RaceResultsPage({ params }: RaceResultsPageProps) {
|
||||
const router = useRouter();
|
||||
export default async function RaceResultsPage({ params }: RaceResultsPageProps) {
|
||||
const raceId = params.id;
|
||||
|
||||
if (!raceId) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const currentDriverId = useEffectiveDriverId() || '';
|
||||
|
||||
// Fetch data using domain hook
|
||||
const { data: queries, isLoading, error, refetch } = useRaceResultsPageData(raceId, currentDriverId);
|
||||
|
||||
// Additional data - league memberships
|
||||
const leagueName = queries?.results?.league?.name || '';
|
||||
const { data: memberships } = useLeagueMemberships(leagueName, currentDriverId);
|
||||
|
||||
// Transform data
|
||||
const data = queries?.results && queries?.sof
|
||||
? RaceResultsDataTransformer.transform(
|
||||
queries.results,
|
||||
queries.sof,
|
||||
currentDriverId,
|
||||
memberships
|
||||
)
|
||||
: undefined;
|
||||
|
||||
// UI State for import functionality
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importSuccess, setImportSuccess] = useState(false);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
const [showImportForm, setShowImportForm] = useState(false);
|
||||
|
||||
// Actions
|
||||
const handleBack = () => router.back();
|
||||
|
||||
const handleImportResults = async (importedResults: any[]) => {
|
||||
setImporting(true);
|
||||
setImportError(null);
|
||||
|
||||
try {
|
||||
console.log('Import results:', importedResults);
|
||||
setImportSuccess(true);
|
||||
|
||||
// Refetch data after import
|
||||
await refetch();
|
||||
} catch (err) {
|
||||
setImportError(err instanceof Error ? err.message : 'Failed to import results');
|
||||
} finally {
|
||||
setImporting(false);
|
||||
// Execute PageQuery
|
||||
const result = await RaceResultsPageQuery.execute({ raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
const error = result.getError();
|
||||
|
||||
switch (error) {
|
||||
case 'notFound':
|
||||
notFound();
|
||||
case 'redirect':
|
||||
notFound();
|
||||
default:
|
||||
// Pass error to template via StatefulPageWrapper
|
||||
return (
|
||||
<StatefulPageWrapper
|
||||
data={null}
|
||||
isLoading={false}
|
||||
error={new Error('Failed to load race results')}
|
||||
retry={() => Promise.resolve()}
|
||||
Template={({ data: _data }) => (
|
||||
<RaceResultsTemplate
|
||||
raceTrack={undefined}
|
||||
raceScheduledAt={undefined}
|
||||
totalDrivers={undefined}
|
||||
leagueName={undefined}
|
||||
raceSOF={null}
|
||||
results={[]}
|
||||
penalties={[]}
|
||||
pointsSystem={{}}
|
||||
fastestLapTime={0}
|
||||
currentDriverId={''}
|
||||
isAdmin={false}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onBack={() => {}}
|
||||
onImportResults={() => Promise.resolve()}
|
||||
onPenaltyClick={() => {}}
|
||||
importing={false}
|
||||
importSuccess={false}
|
||||
importError={null}
|
||||
showImportForm={false}
|
||||
setShowImportForm={() => {}}
|
||||
/>
|
||||
)}
|
||||
loading={{ variant: 'skeleton', message: 'Loading race results...' }}
|
||||
errorConfig={{ variant: 'full-screen' }}
|
||||
empty={{
|
||||
icon: Trophy,
|
||||
title: 'No results available',
|
||||
description: 'Race results will appear here once the race is completed',
|
||||
action: { label: 'Back to Race', onClick: () => {} }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePenaltyClick = (driver: { id: string; name: string }) => {
|
||||
console.log('Penalty click for:', driver);
|
||||
};
|
||||
|
||||
// Determine admin status from memberships data
|
||||
const currentDriver = data?.results.find(r => r.isCurrentUser);
|
||||
const currentMembership = data?.memberships?.find(m => m.driverId === currentDriverId);
|
||||
const isAdmin = currentMembership
|
||||
? LeagueRoleUtility.isLeagueAdminOrHigherRole(currentMembership.role)
|
||||
: false;
|
||||
}
|
||||
|
||||
const viewData = result.unwrap();
|
||||
|
||||
return (
|
||||
<StatefulPageWrapper
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
error={error as Error | null}
|
||||
retry={refetch}
|
||||
Template={({ data }) => (
|
||||
data={viewData}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
retry={() => Promise.resolve()}
|
||||
Template={({ data: _data }) => (
|
||||
<RaceResultsTemplate
|
||||
raceTrack={data.raceTrack}
|
||||
raceScheduledAt={data.raceScheduledAt}
|
||||
totalDrivers={data.totalDrivers}
|
||||
leagueName={data.leagueName}
|
||||
raceSOF={data.raceSOF}
|
||||
results={data.results}
|
||||
penalties={data.penalties}
|
||||
pointsSystem={data.pointsSystem}
|
||||
fastestLapTime={data.fastestLapTime}
|
||||
currentDriverId={currentDriverId}
|
||||
isAdmin={isAdmin}
|
||||
raceTrack={viewData.raceTrack}
|
||||
raceScheduledAt={viewData.raceScheduledAt}
|
||||
totalDrivers={viewData.totalDrivers}
|
||||
leagueName={viewData.leagueName}
|
||||
raceSOF={viewData.raceSOF}
|
||||
results={viewData.results}
|
||||
penalties={viewData.penalties}
|
||||
pointsSystem={viewData.pointsSystem}
|
||||
fastestLapTime={viewData.fastestLapTime}
|
||||
currentDriverId={''}
|
||||
isAdmin={false}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onBack={handleBack}
|
||||
onImportResults={handleImportResults}
|
||||
onPenaltyClick={handlePenaltyClick}
|
||||
importing={importing}
|
||||
importSuccess={importSuccess}
|
||||
importError={importError}
|
||||
showImportForm={showImportForm}
|
||||
setShowImportForm={setShowImportForm}
|
||||
onBack={() => {}}
|
||||
onImportResults={() => Promise.resolve()}
|
||||
onPenaltyClick={() => {}}
|
||||
importing={false}
|
||||
importSuccess={false}
|
||||
importError={null}
|
||||
showImportForm={false}
|
||||
setShowImportForm={() => {}}
|
||||
/>
|
||||
)}
|
||||
loading={{ variant: 'skeleton', message: 'Loading race results...' }}
|
||||
@@ -118,7 +113,7 @@ export default function RaceResultsPage({ params }: RaceResultsPageProps) {
|
||||
icon: Trophy,
|
||||
title: 'No results available',
|
||||
description: 'Race results will appear here once the race is completed',
|
||||
action: { label: 'Back to Race', onClick: handleBack }
|
||||
action: { label: 'Back to Race', onClick: () => {} }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,142 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
import { RaceStewardingTemplate, StewardingTab } from '@/templates/RaceStewardingTemplate';
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient';
|
||||
import { PenaltiesApiClient } from '@/lib/api/penalties/PenaltiesApiClient';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { useLeagueMemberships } from "@/lib/hooks/league/useLeagueMemberships";
|
||||
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
|
||||
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
|
||||
import { RaceStewardingPageQuery } from '@/lib/page-queries/races/RaceStewardingPageQuery';
|
||||
import { Gavel } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
// Define the view model structure locally to avoid type issues
|
||||
interface RaceStewardingViewModel {
|
||||
race: any;
|
||||
league: any;
|
||||
protests: any[];
|
||||
penalties: any[];
|
||||
driverMap: Record<string, any>;
|
||||
pendingProtests: any[];
|
||||
resolvedProtests: any[];
|
||||
pendingCount: number;
|
||||
resolvedCount: number;
|
||||
penaltiesCount: number;
|
||||
interface RaceStewardingPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function RaceStewardingPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const raceId = params.id as string;
|
||||
const currentDriverId = useEffectiveDriverId() || '';
|
||||
export default function RaceStewardingPage({ params }: RaceStewardingPageProps) {
|
||||
const raceId = params.id;
|
||||
const [activeTab, setActiveTab] = useState<StewardingTab>('pending');
|
||||
|
||||
if (!raceId) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Data state
|
||||
const [pageData, setPageData] = useState<RaceStewardingViewModel | null>(null);
|
||||
const [pageData, setPageData] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// UI State
|
||||
const [activeTab, setActiveTab] = useState<StewardingTab>('pending');
|
||||
|
||||
// Fetch data on mount and when raceId/currentDriverId changes
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
if (!raceId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Manual wiring: 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 racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
const protestsApiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
|
||||
const penaltiesApiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Fetch data in parallel
|
||||
const [raceDetail, protests, penalties] = await Promise.all([
|
||||
racesApiClient.getDetail(raceId, currentDriverId),
|
||||
protestsApiClient.getRaceProtests(raceId),
|
||||
penaltiesApiClient.getRacePenalties(raceId),
|
||||
]);
|
||||
|
||||
// Transform data to match view model structure
|
||||
const data: RaceStewardingViewModel = {
|
||||
race: raceDetail.race,
|
||||
league: raceDetail.league,
|
||||
protests: protests.protests.map(p => ({
|
||||
id: p.id,
|
||||
protestingDriverId: p.protestingDriverId,
|
||||
accusedDriverId: p.accusedDriverId,
|
||||
incident: {
|
||||
lap: p.lap,
|
||||
description: p.description,
|
||||
},
|
||||
filedAt: p.filedAt,
|
||||
status: p.status,
|
||||
})),
|
||||
penalties: penalties.penalties,
|
||||
driverMap: { ...protests.driverMap, ...penalties.driverMap },
|
||||
pendingProtests: [],
|
||||
resolvedProtests: [],
|
||||
pendingCount: 0,
|
||||
resolvedCount: 0,
|
||||
penaltiesCount: 0,
|
||||
};
|
||||
|
||||
// Calculate derived properties
|
||||
data.pendingProtests = data.protests.filter(p => p.status === 'pending' || p.status === 'under_review');
|
||||
data.resolvedProtests = data.protests.filter(p =>
|
||||
p.status === 'upheld' ||
|
||||
p.status === 'dismissed' ||
|
||||
p.status === 'withdrawn'
|
||||
);
|
||||
data.pendingCount = data.pendingProtests.length;
|
||||
data.resolvedCount = data.resolvedProtests.length;
|
||||
data.penaltiesCount = data.penalties.length;
|
||||
|
||||
if (data) {
|
||||
setPageData(data);
|
||||
} else {
|
||||
setPageData(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to fetch stewarding data'));
|
||||
setPageData(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
// Fetch function
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetchData();
|
||||
}, [raceId, currentDriverId]);
|
||||
|
||||
// Fetch membership
|
||||
const { data: membershipsData } = useLeagueMemberships(pageData?.league?.id || '', currentDriverId || '');
|
||||
const currentMembership = membershipsData?.members.find(m => m.driverId === currentDriverId);
|
||||
const isAdmin = currentMembership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(currentMembership.role) : false;
|
||||
|
||||
// Actions
|
||||
const handleBack = () => {
|
||||
router.push(`/races/${raceId}`);
|
||||
};
|
||||
|
||||
const handleReviewProtest = (protestId: string) => {
|
||||
// Navigate to protest review page
|
||||
router.push(`/leagues/${pageData?.league?.id}/stewarding/protests/${protestId}`);
|
||||
try {
|
||||
const result = await RaceStewardingPageQuery.execute({ raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error('Failed to fetch stewarding data');
|
||||
}
|
||||
|
||||
setPageData(result.unwrap());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Unknown error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Transform data for template
|
||||
@@ -152,74 +57,14 @@ export default function RaceStewardingPage() {
|
||||
penaltiesCount: pageData.penaltiesCount,
|
||||
} : undefined;
|
||||
|
||||
const retry = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Manual wiring: create dependencies
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
// Actions
|
||||
const handleBack = () => {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
// Create API clients
|
||||
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
const protestsApiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
|
||||
const penaltiesApiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Fetch data in parallel
|
||||
const [raceDetail, protests, penalties] = await Promise.all([
|
||||
racesApiClient.getDetail(raceId, currentDriverId),
|
||||
protestsApiClient.getRaceProtests(raceId),
|
||||
penaltiesApiClient.getRacePenalties(raceId),
|
||||
]);
|
||||
|
||||
// Transform data to match view model structure
|
||||
const data: RaceStewardingViewModel = {
|
||||
race: raceDetail.race,
|
||||
league: raceDetail.league,
|
||||
protests: protests.protests.map(p => ({
|
||||
id: p.id,
|
||||
protestingDriverId: p.protestingDriverId,
|
||||
accusedDriverId: p.accusedDriverId,
|
||||
incident: {
|
||||
lap: p.lap,
|
||||
description: p.description,
|
||||
},
|
||||
filedAt: p.filedAt,
|
||||
status: p.status,
|
||||
})),
|
||||
penalties: penalties.penalties,
|
||||
driverMap: { ...protests.driverMap, ...penalties.driverMap },
|
||||
pendingProtests: [],
|
||||
resolvedProtests: [],
|
||||
pendingCount: 0,
|
||||
resolvedCount: 0,
|
||||
penaltiesCount: 0,
|
||||
};
|
||||
|
||||
// Calculate derived properties
|
||||
data.pendingProtests = data.protests.filter(p => p.status === 'pending' || p.status === 'under_review');
|
||||
data.resolvedProtests = data.protests.filter(p =>
|
||||
p.status === 'upheld' ||
|
||||
p.status === 'dismissed' ||
|
||||
p.status === 'withdrawn'
|
||||
);
|
||||
data.pendingCount = data.pendingProtests.length;
|
||||
data.resolvedCount = data.resolvedProtests.length;
|
||||
data.penaltiesCount = data.penalties.length;
|
||||
|
||||
if (data) {
|
||||
setPageData(data);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to fetch stewarding data'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
const handleReviewProtest = (protestId: string) => {
|
||||
if (templateData?.league?.id) {
|
||||
window.location.href = `/leagues/${templateData.league.id}/stewarding/protests/${protestId}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,15 +73,15 @@ export default function RaceStewardingPage() {
|
||||
data={pageData}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
retry={retry}
|
||||
Template={({ data }) => (
|
||||
retry={fetchData}
|
||||
Template={({ data: _data }) => (
|
||||
<RaceStewardingTemplate
|
||||
stewardingData={templateData}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onBack={handleBack}
|
||||
onReviewProtest={handleReviewProtest}
|
||||
isAdmin={isAdmin}
|
||||
isAdmin={false}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
|
||||
@@ -1,30 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
|
||||
import { RacesAllTemplate, StatusFilter } from '@/templates/RacesAllTemplate';
|
||||
import { useAllRacesPageData } from "@/lib/hooks/race/useAllRacesPageData";
|
||||
import { RacesAllTemplate } from '@/templates/RacesAllTemplate';
|
||||
import { RacesAllPageQuery } from '@/lib/page-queries/races/RacesAllPageQuery';
|
||||
import { Flag } from 'lucide-react';
|
||||
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
|
||||
interface Race {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
|
||||
sessionType: string;
|
||||
leagueId?: string;
|
||||
leagueName?: string;
|
||||
strengthOfField?: number;
|
||||
}
|
||||
|
||||
export default function RacesAllPage() {
|
||||
const router = useRouter();
|
||||
|
||||
// Client-side state for filters and pagination
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'>('all');
|
||||
const [leagueFilter, setLeagueFilter] = useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
|
||||
// Fetch data using domain hook
|
||||
const { data: pageData, isLoading, error, refetch } = useAllRacesPageData();
|
||||
// Data state
|
||||
const [pageData, setPageData] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// Transform data for template
|
||||
const races = pageData?.races.map((race) => ({
|
||||
// Fetch data
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await RacesAllPageQuery.execute();
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error('Failed to fetch races');
|
||||
}
|
||||
|
||||
setPageData(result.unwrap());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Unknown error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch on mount
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Transform data
|
||||
const races: Race[] = pageData?.races.map((race: any) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
@@ -36,8 +75,8 @@ export default function RacesAllPage() {
|
||||
strengthOfField: race.strengthOfField ?? undefined,
|
||||
})) ?? [];
|
||||
|
||||
// Calculate total pages
|
||||
const filteredRaces = races.filter((race) => {
|
||||
// Filter and paginate (Note: This should be done by API per contract)
|
||||
const filteredRaces = races.filter((race: Race) => {
|
||||
if (statusFilter !== 'all' && race.status !== statusFilter) {
|
||||
return false;
|
||||
}
|
||||
@@ -60,6 +99,7 @@ export default function RacesAllPage() {
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(filteredRaces.length / ITEMS_PER_PAGE);
|
||||
const paginatedRaces = filteredRaces.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
|
||||
|
||||
// Actions
|
||||
const handleRaceClick = (raceId: string) => {
|
||||
@@ -79,10 +119,10 @@ export default function RacesAllPage() {
|
||||
data={pageData}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
retry={refetch}
|
||||
Template={({ data }) => (
|
||||
retry={fetchData}
|
||||
Template={({ data: _data }) => (
|
||||
<RacesAllTemplate
|
||||
races={races}
|
||||
races={paginatedRaces}
|
||||
isLoading={false}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
|
||||
@@ -1,53 +1,55 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { RacesTemplate } from '@/templates/RacesTemplate';
|
||||
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 { RacesPageQuery } from '@/lib/page-queries/races/RacesPageQuery';
|
||||
|
||||
export default async function Page() {
|
||||
// Manual wiring: 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 client
|
||||
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Fetch data
|
||||
const data = await racesApiClient.getPageData();
|
||||
const result = await RacesPageQuery.execute();
|
||||
|
||||
// Transform races
|
||||
const transformRace = (race: any) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
|
||||
sessionType: 'race',
|
||||
leagueId: race.leagueId,
|
||||
leagueName: race.leagueName,
|
||||
strengthOfField: race.strengthOfField ?? undefined,
|
||||
isUpcoming: race.status === 'scheduled',
|
||||
isLive: race.status === 'running',
|
||||
isPast: race.status === 'completed',
|
||||
});
|
||||
|
||||
const races = data.races.map(transformRace);
|
||||
const scheduledRaces = races.filter(r => r.isUpcoming);
|
||||
const runningRaces = races.filter(r => r.isLive);
|
||||
const completedRaces = races.filter(r => r.isPast);
|
||||
const totalCount = races.length;
|
||||
if (result.isErr()) {
|
||||
const error = result.getError();
|
||||
|
||||
switch (error) {
|
||||
case 'notFound':
|
||||
notFound();
|
||||
case 'redirect':
|
||||
// Would redirect to login or other page
|
||||
notFound();
|
||||
default:
|
||||
// For other errors, show error state in template
|
||||
return <RacesTemplate
|
||||
races={[]}
|
||||
totalCount={0}
|
||||
scheduledRaces={[]}
|
||||
runningRaces={[]}
|
||||
completedRaces={[]}
|
||||
isLoading={false}
|
||||
statusFilter="all"
|
||||
setStatusFilter={() => {}}
|
||||
leagueFilter="all"
|
||||
setLeagueFilter={() => {}}
|
||||
timeFilter="upcoming"
|
||||
setTimeFilter={() => {}}
|
||||
onRaceClick={() => {}}
|
||||
onLeagueClick={() => {}}
|
||||
onRegister={() => {}}
|
||||
onWithdraw={() => {}}
|
||||
onCancel={() => {}}
|
||||
showFilterModal={false}
|
||||
setShowFilterModal={() => {}}
|
||||
currentDriverId={undefined}
|
||||
userMemberships={[]}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const viewData = result.unwrap();
|
||||
|
||||
return <RacesTemplate
|
||||
races={races}
|
||||
totalCount={totalCount}
|
||||
scheduledRaces={scheduledRaces}
|
||||
runningRaces={runningRaces}
|
||||
completedRaces={completedRaces}
|
||||
races={viewData.races}
|
||||
totalCount={viewData.totalCount}
|
||||
scheduledRaces={viewData.scheduledRaces}
|
||||
runningRaces={viewData.runningRaces}
|
||||
completedRaces={viewData.completedRaces}
|
||||
isLoading={false}
|
||||
statusFilter="all"
|
||||
setStatusFilter={() => {}}
|
||||
|
||||
Reference in New Issue
Block a user