website refactor

This commit is contained in:
2026-01-14 23:46:04 +01:00
parent c1a86348d7
commit 4a2d7d15a5
294 changed files with 5637 additions and 3418 deletions

View File

@@ -0,0 +1,65 @@
'use client';
import { useState, useMemo } from 'react';
import { RacesTemplate, type TimeFilter, type RaceStatusFilter } from '@/templates/RacesTemplate';
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
interface RacesPageClientProps {
viewData: RacesViewData;
}
export function RacesPageClient({ viewData }: RacesPageClientProps) {
const [statusFilter, setStatusFilter] = useState<RaceStatusFilter>('all');
const [leagueFilter, setLeagueFilter] = useState<string>('all');
const [timeFilter, setTimeFilter] = useState<TimeFilter>('upcoming');
const [showFilterModal, setShowFilterModal] = useState(false);
const filteredRaces = useMemo(() => {
return viewData.races.filter((race) => {
if (statusFilter !== 'all' && race.status !== statusFilter) return false;
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) return false;
if (timeFilter === 'upcoming' && !race.isUpcoming) return false;
if (timeFilter === 'live' && !race.isLive) return false;
if (timeFilter === 'past' && !race.isPast) return false;
return true;
});
}, [viewData.races, statusFilter, leagueFilter, timeFilter]);
const racesByDate = useMemo(() => {
const grouped = new Map<string, typeof filteredRaces[0][]>();
filteredRaces.forEach((race) => {
const dateKey = race.scheduledAt.split('T')[0]!;
if (!grouped.has(dateKey)) {
grouped.set(dateKey, []);
}
grouped.get(dateKey)!.push(race);
});
return Array.from(grouped.entries()).map(([dateKey, dayRaces]) => ({
dateKey,
dateLabel: dayRaces[0]?.scheduledAtLabel || '',
races: dayRaces,
}));
}, [filteredRaces]);
return (
<RacesTemplate
viewData={{
...viewData,
races: filteredRaces,
racesByDate,
}}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
showFilterModal={showFilterModal}
setShowFilterModal={setShowFilterModal}
onRaceClick={(id) => console.log('Race click', id)}
onLeagueClick={(id) => console.log('League click', id)}
onWithdraw={(id) => console.log('Withdraw', id)}
onCancel={(id) => console.log('Cancel', id)}
/>
);
}

View File

@@ -0,0 +1,85 @@
'use client';
import React, { useState, useEffect } from 'react';
import { RaceDetailTemplate, type RaceDetailViewData } from '@/templates/RaceDetailTemplate';
interface RaceDetailPageClientProps {
viewData: RaceDetailViewData;
onBack: () => void;
onRegister: () => void;
onWithdraw: () => void;
onCancel: () => void;
onReopen: () => void;
onEndRace: () => void;
onFileProtest: () => void;
onResultsClick: () => void;
onStewardingClick: () => void;
onLeagueClick: (id: string) => void;
onDriverClick: (id: string) => void;
isOwnerOrAdmin: boolean;
}
export function RaceDetailPageClient({
viewData,
onBack,
onRegister,
onWithdraw,
onCancel,
onReopen,
onEndRace,
onFileProtest,
onResultsClick,
onStewardingClick,
onLeagueClick,
onDriverClick,
isOwnerOrAdmin
}: RaceDetailPageClientProps) {
const [showProtestModal, setShowProtestModal] = useState(false);
const [showEndRaceModal, setShowEndRaceModal] = useState(false);
const [animatedRatingChange, setAnimatedRatingChange] = useState(0);
const ratingChange = viewData.userResult?.ratingChange ?? null;
useEffect(() => {
if (ratingChange !== null) {
let start = 0;
const end = ratingChange;
const duration = 1000;
const startTime = performance.now();
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.round(start + (end - start) * eased);
setAnimatedRatingChange(current);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
}, [ratingChange]);
return (
<RaceDetailTemplate
viewData={viewData}
isLoading={false}
onBack={onBack}
onRegister={onRegister}
onWithdraw={onWithdraw}
onCancel={onCancel}
onReopen={onReopen}
onEndRace={onEndRace}
onFileProtest={onFileProtest}
onResultsClick={onResultsClick}
onStewardingClick={onStewardingClick}
onLeagueClick={onLeagueClick}
onDriverClick={onDriverClick}
isOwnerOrAdmin={isOwnerOrAdmin}
animatedRatingChange={animatedRatingChange}
/>
);
}

View File

@@ -34,7 +34,7 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
data={null}
Template={({ data: _data }) => (
<RaceDetailTemplate
viewModel={undefined}
viewData={undefined}
isLoading={false}
error={new Error('Failed to load race details')}
onBack={() => {}}
@@ -48,12 +48,8 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
onStewardingClick={() => {}}
onLeagueClick={() => {}}
onDriverClick={() => {}}
currentDriverId={''}
isOwnerOrAdmin={false}
showProtestModal={false}
setShowProtestModal={() => {}}
showEndRaceModal={false}
setShowEndRaceModal={() => {}}
animatedRatingChange={0}
mutationLoading={{
register: false,
withdraw: false,
@@ -78,23 +74,12 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
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={viewData}
Template={({ data: _data }) => (
<RaceDetailTemplate
viewModel={viewModel}
viewData={viewData}
isLoading={false}
error={null}
onBack={() => {}}
@@ -108,12 +93,8 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
onStewardingClick={() => {}}
onLeagueClick={() => {}}
onDriverClick={() => {}}
currentDriverId={''}
isOwnerOrAdmin={false}
showProtestModal={false}
setShowProtestModal={() => {}}
showEndRaceModal={false}
setShowEndRaceModal={() => {}}
animatedRatingChange={0}
mutationLoading={{
register: false,
withdraw: false,

View File

@@ -1,9 +1,10 @@
import { notFound } from 'next/navigation';
import { RacesTemplate } from '@/templates/RacesTemplate';
import { RacesPageQuery } from '@/lib/page-queries/races/RacesPageQuery';
import { RacesPageClient } from './RacesPageClient';
export default async function Page() {
const result = await RacesPageQuery.execute();
const query = new RacesPageQuery();
const result = await query.execute();
if (result.isErr()) {
const error = result.getError();
@@ -12,59 +13,13 @@ export default async function Page() {
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={[]}
/>;
notFound();
}
}
const viewData = result.unwrap();
return <RacesTemplate
races={viewData.races}
totalCount={viewData.totalCount}
scheduledRaces={viewData.scheduledRaces}
runningRaces={viewData.runningRaces}
completedRaces={viewData.completedRaces}
isLoading={false}
statusFilter="all"
setStatusFilter={() => {}}
leagueFilter="all"
setLeagueFilter={() => {}}
timeFilter="upcoming"
setTimeFilter={() => {}}
onRaceClick={() => {}}
onLeagueClick={() => {}}
onRegister={() => {}}
onWithdraw={() => {}}
onCancel={() => {}}
showFilterModal={false}
setShowFilterModal={() => {}}
currentDriverId={undefined}
userMemberships={[]}
/>;
}
return <RacesPageClient viewData={viewData} />;
}