From ac37871bef9f63ea157d80e50adb3ff8b7e7db6c Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 21 Jan 2026 13:49:59 +0100 Subject: [PATCH] website refactor --- apps/website/app/leagues/[id]/roster/page.tsx | 50 +--- .../app/leagues/[id]/standings/page.tsx | 6 +- .../leagues/LeagueSchedulePanel.tsx | 95 ++----- .../leagues/LeagueStandingsTable.tsx | 86 ++---- .../components/leagues/RosterTable.tsx | 94 ++----- apps/website/components/races/PointsTable.tsx | 10 +- .../components/races/RaceScheduleTable.tsx | 12 +- .../lib/display-objects/DateDisplay.ts | 11 + .../templates/LeagueScheduleTemplate.tsx | 6 +- .../templates/LeagueStandingsTemplate.tsx | 3 +- apps/website/templates/RulebookTemplate.tsx | 245 +++++++++++++----- 11 files changed, 280 insertions(+), 338 deletions(-) diff --git a/apps/website/app/leagues/[id]/roster/page.tsx b/apps/website/app/leagues/[id]/roster/page.tsx index 24d3dacc2..6bdaab049 100644 --- a/apps/website/app/leagues/[id]/roster/page.tsx +++ b/apps/website/app/leagues/[id]/roster/page.tsx @@ -3,6 +3,9 @@ import { notFound } from 'next/navigation'; import { Box } from '@/ui/Box'; import { Text } from '@/ui/Text'; import { Stack } from '@/ui/Stack'; +import { RosterTable } from '@/components/leagues/RosterTable'; + +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; interface Props { params: Promise<{ id: string }>; @@ -17,7 +20,13 @@ export default async function LeagueRosterPage({ params }: Props) { } const data = result.unwrap(); - const members = data.memberships.members || []; + const members = (data.memberships.members || []).map(m => ({ + driverId: m.driverId, + driverName: m.driver.name, + role: m.role, + joinedAt: m.joinedAt, + joinedAtLabel: DateDisplay.formatShort(m.joinedAt) + })); return ( @@ -26,42 +35,7 @@ export default async function LeagueRosterPage({ params }: Props) { All drivers currently registered in this league. - - - - - - - - - - - {members.map((member) => ( - - - - - - ))} - {members.length === 0 && ( - - - - )} - -
DriverRoleJoined
- - - {member.driver.name} - - - {member.role} - - {new Date(member.joinedAt).toLocaleDateString()} -
- No members found in this league. -
-
+
); -} \ No newline at end of file +} diff --git a/apps/website/app/leagues/[id]/standings/page.tsx b/apps/website/app/leagues/[id]/standings/page.tsx index eebdf88c5..728b6cd5a 100644 --- a/apps/website/app/leagues/[id]/standings/page.tsx +++ b/apps/website/app/leagues/[id]/standings/page.tsx @@ -30,14 +30,10 @@ export default async function Page({ params }: Props) { currentDriverId: null, isAdmin: false, }} - onRemoveMember={() => {}} - onUpdateRole={() => {}} />; } return {}} - onUpdateRole={() => {}} />; -} \ No newline at end of file +} diff --git a/apps/website/components/leagues/LeagueSchedulePanel.tsx b/apps/website/components/leagues/LeagueSchedulePanel.tsx index 142fcf7ac..813922bdb 100644 --- a/apps/website/components/leagues/LeagueSchedulePanel.tsx +++ b/apps/website/components/leagues/LeagueSchedulePanel.tsx @@ -1,9 +1,8 @@ 'use client'; -import { Heading } from '@/ui/Heading'; -import { Stack } from '@/ui/Stack'; -import { Text } from '@/ui/Text'; -import { Clock, MapPin } from 'lucide-react'; +import { RaceScheduleTable } from '@/components/races/RaceScheduleTable'; +import { useRouter } from 'next/navigation'; +import { routes } from '@/lib/routing/RouteConfig'; interface RaceEvent { id: string; @@ -20,79 +19,21 @@ interface LeagueSchedulePanelProps { } export function LeagueSchedulePanel({ events }: LeagueSchedulePanelProps) { + const router = useRouter(); + + const races = events.map(event => ({ + id: event.id, + track: event.trackName, + car: 'TBA', // Not provided in event + leagueName: null, + time: event.time, + status: (event.status === 'completed' ? 'completed' : 'scheduled') as any + })); + return ( - - - {events.map((event) => ( - - - - {new Date(event.date).toLocaleDateString('en-US', { month: 'short' })} - - - {new Date(event.date).toLocaleDateString('en-US', { day: 'numeric' })} - - - - - {event.title} - - - - {event.trackName} - - - - {event.time} - - {event.strengthOfField && ( - - SOF - {event.strengthOfField} - - )} - - - - - {event.status === 'live' && ( - - - - Live - - - )} - {event.status === 'upcoming' && ( - - - Upcoming - - - )} - {event.status === 'completed' && ( - - - Results - - - )} - - - ))} - - + router.push(routes.race.detail(id))} + /> ); } diff --git a/apps/website/components/leagues/LeagueStandingsTable.tsx b/apps/website/components/leagues/LeagueStandingsTable.tsx index 1d1a4f095..522211a4c 100644 --- a/apps/website/components/leagues/LeagueStandingsTable.tsx +++ b/apps/website/components/leagues/LeagueStandingsTable.tsx @@ -1,12 +1,15 @@ 'use client'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table'; -import { Text } from '@/ui/Text'; -import { Stack } from '@/ui/Stack'; +import { LeaderboardTableShell } from '@/ui/LeaderboardTableShell'; +import { LeaderboardList } from '@/ui/LeaderboardList'; +import { RankingRow } from '@/components/leaderboards/RankingRow'; +import { useRouter } from 'next/navigation'; +import { routes } from '@/lib/routing/RouteConfig'; interface StandingEntry { position: number; driverName: string; + driverId?: string; // Added to support navigation teamName?: string; points: number; wins: number; @@ -21,62 +24,27 @@ interface LeagueStandingsTableProps { } export function LeagueStandingsTable({ standings }: LeagueStandingsTableProps) { + const router = useRouter(); + return ( - - - - - - Pos - - - Driver - - - Team - - - Races - - - Avg - - - Points - - - Gap - - - - - {standings.map((entry) => ( - - - {entry.position} - - - {entry.driverName} - - - {entry.teamName || '—'} - - - {entry.races} - - - {entry.avgFinish?.toFixed(1) || '—'} - - - {entry.points} - - - {entry.gap} - - - ))} - -
-
+ + + {standings.map((entry) => ( + router.push(routes.driver.detail(entry.driverId!)) : undefined} + /> + ))} + + ); } diff --git a/apps/website/components/leagues/RosterTable.tsx b/apps/website/components/leagues/RosterTable.tsx index 4863700dd..014221ac1 100644 --- a/apps/website/components/leagues/RosterTable.tsx +++ b/apps/website/components/leagues/RosterTable.tsx @@ -1,84 +1,26 @@ -import { Stack } from '@/ui/Stack'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table'; -import { Text } from '@/ui/Text'; +import { TeamMembersTable } from '@/components/teams/TeamMembersTable'; import { ReactNode } from 'react'; +interface Member { + driverId: string; + driverName: string; + role: string; + joinedAt: string; + joinedAtLabel: string; +} + interface RosterTableProps { - children: ReactNode; - columns?: string[]; + members: Member[]; + isAdmin?: boolean; + onRemoveMember?: (driverId: string) => void; } -export function RosterTable({ children, columns = ['Driver', 'Role', 'Joined', 'Rating', 'Rank'] }: RosterTableProps) { +export function RosterTable({ members, isAdmin, onRemoveMember }: RosterTableProps) { return ( - - - - - {columns.map((col) => ( - - - {col} - - - ))} - - {null} - - - - - {children} - -
-
- ); -} - -interface RosterTableRowProps { - driver: ReactNode; - role: ReactNode; - joined: string; - rating: ReactNode; - rank: ReactNode; - actions?: ReactNode; - onClick?: () => void; -} - -export function RosterTableRow({ - driver, - role, - joined, - rating, - rank, - actions, - onClick, -}: RosterTableRowProps) { - return ( - - - {driver} - - - {role} - - - {joined} - - - {rating} - - - {rank} - - - - {actions} - - - + ); } diff --git a/apps/website/components/races/PointsTable.tsx b/apps/website/components/races/PointsTable.tsx index 3394fc73f..04cc7cf09 100644 --- a/apps/website/components/races/PointsTable.tsx +++ b/apps/website/components/races/PointsTable.tsx @@ -1,7 +1,7 @@ import { Box } from '@/ui/Box'; import { Card } from '@/ui/Card'; import { Heading } from '@/ui/Heading'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table'; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; interface PointsTableProps { @@ -16,10 +16,8 @@ export function PointsTable({ title = 'Points Distribution', points }: PointsTab - - Position - Points - + Position + Points {points.map(({ position, points: pts }) => ( @@ -50,7 +48,7 @@ export function PointsTable({ title = 'Points Distribution', points }: PointsTab - + {pts} pts diff --git a/apps/website/components/races/RaceScheduleTable.tsx b/apps/website/components/races/RaceScheduleTable.tsx index 2c3bb0391..38b5b18ab 100644 --- a/apps/website/components/races/RaceScheduleTable.tsx +++ b/apps/website/components/races/RaceScheduleTable.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Text } from '@/ui/Text'; -import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table'; +import { Table, TableHead, TableBody, TableRow, TableHeaderCell, TableCell } from '@/ui/Table'; import { SessionStatusBadge, type SessionStatus } from './SessionStatusBadge'; import { Stack } from '@/ui/Stack'; @@ -24,12 +24,10 @@ export function RaceScheduleTable({ races, onRaceClick }: RaceScheduleTableProps return (
- - Time - Session Details - League - Status - + Time + Session Details + League + Status {races.map((race) => ( diff --git a/apps/website/lib/display-objects/DateDisplay.ts b/apps/website/lib/display-objects/DateDisplay.ts index f834719c4..c9b3c6fa4 100644 --- a/apps/website/lib/display-objects/DateDisplay.ts +++ b/apps/website/lib/display-objects/DateDisplay.ts @@ -35,4 +35,15 @@ export class DateDisplay { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return `${months[d.getUTCMonth()]} ${d.getUTCDate()}`; } + + /** + * Formats a date as "Jan 18, 15:00" using UTC. + */ + static formatDateTime(date: string | Date): string { + const d = typeof date === 'string' ? new Date(date) : date; + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const hours = d.getUTCHours().toString().padStart(2, '0'); + const minutes = d.getUTCMinutes().toString().padStart(2, '0'); + return `${months[d.getUTCMonth()]} ${d.getUTCDate()}, ${hours}:${minutes}`; + } } diff --git a/apps/website/templates/LeagueScheduleTemplate.tsx b/apps/website/templates/LeagueScheduleTemplate.tsx index d9d625a67..8fed33d5c 100644 --- a/apps/website/templates/LeagueScheduleTemplate.tsx +++ b/apps/website/templates/LeagueScheduleTemplate.tsx @@ -5,6 +5,8 @@ import type { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueSched import { Box } from '@/ui/Box'; import { Text } from '@/ui/Text'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; + interface LeagueScheduleTemplateProps { viewData: LeagueScheduleViewData; } @@ -15,8 +17,8 @@ export function LeagueScheduleTemplate({ viewData }: LeagueScheduleTemplateProps title: race.name || `Race ${race.id.substring(0, 4)}`, trackName: race.track || 'TBA', date: race.scheduledAt, - time: new Date(race.scheduledAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }), - status: (race.status as 'upcoming' | 'live' | 'completed') || 'upcoming', + time: DateDisplay.formatDateTime(race.scheduledAt), + status: (race.status === 'completed' ? 'completed' : 'upcoming') as any, strengthOfField: race.strengthOfField })); diff --git a/apps/website/templates/LeagueStandingsTemplate.tsx b/apps/website/templates/LeagueStandingsTemplate.tsx index 0f60d3940..85267d4bd 100644 --- a/apps/website/templates/LeagueStandingsTemplate.tsx +++ b/apps/website/templates/LeagueStandingsTemplate.tsx @@ -7,8 +7,6 @@ import { Text } from '@/ui/Text'; interface LeagueStandingsTemplateProps { viewData: LeagueStandingsViewData; - onRemoveMember: (driverId: string) => void; - onUpdateRole: (driverId: string, newRole: string) => void; loading?: boolean; } @@ -31,6 +29,7 @@ export function LeagueStandingsTemplate({ return { position: entry.position, driverName: driver?.name || 'Unknown Driver', + driverId: entry.driverId, points: entry.totalPoints, wins: 0, // Placeholder podiums: 0, // Placeholder diff --git a/apps/website/templates/RulebookTemplate.tsx b/apps/website/templates/RulebookTemplate.tsx index fe247ae9f..b11f6d5cf 100644 --- a/apps/website/templates/RulebookTemplate.tsx +++ b/apps/website/templates/RulebookTemplate.tsx @@ -1,86 +1,199 @@ 'use client'; -import { LeagueRulesPanel } from '@/components/leagues/LeagueRulesPanel'; +import { RulebookTabs, type RulebookSection } from '@/components/leagues/RulebookTabs'; +import { PointsTable } from '@/components/races/PointsTable'; import type { RulebookViewData } from '@/lib/view-data/leagues/RulebookViewData'; import { Box } from '@/ui/Box'; +import { Heading } from '@/ui/Heading'; +import { Icon } from '@/ui/Icon'; +import { Grid } from '@/ui/Grid'; +import { Stack } from '@/ui/Stack'; +import { Surface } from '@/ui/Surface'; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; +import { AlertTriangle, Book, Clock, Info, Scale, Shield, type LucideIcon } from 'lucide-react'; +import { useState } from 'react'; interface RulebookTemplateProps { viewData: RulebookViewData; } -export function RulebookTemplate({ viewData }: RulebookTemplateProps) { - const rules = [ - { - id: 'points', - title: 'Points System', - content: `Points are awarded to the top ${viewData.positionPoints.length} finishers. 1st place receives ${viewData.positionPoints[0]?.points || 0} points.` - }, - { - id: 'drops', - title: 'Drop Policy', - content: viewData.hasActiveDropPolicy ? viewData.dropPolicySummary : 'No drop races are active for this season.' - }, - { - id: 'platform', - title: 'Platform & Sessions', - content: `Racing on ${viewData.gameName}. Sessions scored: ${viewData.sessionTypes}.` - } - ]; +export function RulebookTemplate({ + viewData, +}: RulebookTemplateProps) { + const [activeSection, setActiveSection] = useState('scoring'); - if (viewData.hasBonusPoints) { - rules.push({ - id: 'bonus', - title: 'Bonus Points', - content: viewData.bonusPoints.join('. ') - }); + if (!viewData) { + return ( + + + + Unable to load rulebook + + + ); } return ( - - - - Rulebook - - - {viewData.scoringPresetName || 'Custom Rules'} - - - - Official rules and regulations for this championship. - + + {/* Navigation Tabs */} + - + {/* Content Sections */} + {activeSection === 'scoring' && ( + + {/* Quick Stats */} + + + + + + - - Points Classification - - - - - - Position - - - Points - - - - - {viewData.positionPoints.map((point) => ( - - - {point.position} - - - {point.points} - - - ))} - - + {/* Weekend Structure */} + + + + + WEEKEND STRUCTURE + + + + + + + + + + + {/* Points Table */} + + + {/* Bonus Points */} + {viewData.hasBonusPoints && ( + + + BONUS POINTS + + {viewData.bonusPoints.map((bonus, idx) => ( + + + + + + + {bonus} + + + ))} + + + + )} + + )} + + {activeSection === 'conduct' && ( + + + + + DRIVER CONDUCT + + + + + + + + + + )} + + {activeSection === 'protests' && ( + + + + + PROTEST PROCESS + + + + + + + + + )} + + {activeSection === 'penalties' && ( + + + + + PENALTY GUIDELINES + +
+ + Infraction + Typical Penalty + + + + + + + +
+
+ + )} + + ); +} + +function StatItem({ icon, label, value, color }: { icon: LucideIcon, label: string, value: string | number, color: string }) { + return ( + + + + - + + {label.toUpperCase()} + {value} + + + + ); +} + +function TimingItem({ label, value }: { label: string, value: string }) { + return ( + + {label} + {value} ); } + +function ConductItem({ number, title, text }: { number: number, title: string, text: string }) { + return ( + + {number}. {title} + {text} + + ); +} + +function PenaltyRow({ infraction, penalty, isSevere }: { infraction: string, penalty: string, isSevere?: boolean }) { + return ( + + + {infraction} + + + {penalty} + + + ); +}