website refactor

This commit is contained in:
2026-01-21 17:50:02 +01:00
parent 4b54c3603b
commit 02987f60c8
29 changed files with 1673 additions and 35 deletions

View File

@@ -1,6 +1,10 @@
'use client';
import { AdminQuickViewWidgets } from '@/components/leagues/AdminQuickViewWidgets';
import { LeagueActivityFeed } from '@/components/leagues/LeagueActivityFeed';
import { LeagueLogo } from '@/components/leagues/LeagueLogo';
import { NextRaceCountdownWidget } from '@/components/leagues/NextRaceCountdownWidget';
import { SeasonProgressWidget } from '@/components/leagues/SeasonProgressWidget';
import type { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
@@ -10,18 +14,19 @@ import { Calendar, Shield, Trophy, Users, type LucideIcon } from 'lucide-react';
interface LeagueOverviewTemplateProps {
viewData: LeagueDetailViewData;
isOwnerOrAdmin: boolean;
}
export function LeagueOverviewTemplate({ viewData }: LeagueOverviewTemplateProps) {
export function LeagueOverviewTemplate({ viewData, isOwnerOrAdmin }: LeagueOverviewTemplateProps) {
return (
<Stack gap={8}>
{/* Header with Logo */}
<Box display="flex" alignItems="center" gap={6} pb={8} borderBottom borderColor="zinc-800">
<LeagueLogo
leagueId={viewData.leagueId}
src={viewData.logoUrl}
alt={viewData.name}
size={96}
<LeagueLogo
leagueId={viewData.leagueId}
src={viewData.logoUrl}
alt={viewData.name}
size={96}
rounded="lg"
/>
<Stack gap={2}>
@@ -34,6 +39,41 @@ export function LeagueOverviewTemplate({ viewData }: LeagueOverviewTemplateProps
{/* Main Content */}
<Box responsiveColSpan={{ lg: 2 }}>
<Stack gap={8}>
{/* Next Race Section */}
{viewData.nextRace && (
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Next Race</Text>
<NextRaceCountdownWidget
raceId={viewData.nextRace.id}
raceName={viewData.nextRace.name}
date={viewData.nextRace.date}
track={viewData.nextRace.track}
car={viewData.nextRace.car}
/>
</Stack>
)}
{/* Season Progress Section */}
{viewData.seasonProgress && (
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Season Progress</Text>
<SeasonProgressWidget
completedRaces={viewData.seasonProgress.completedRaces}
totalRaces={viewData.seasonProgress.totalRaces}
percentage={viewData.seasonProgress.percentage}
/>
</Stack>
)}
{/* League Activity Feed */}
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Recent Activity</Text>
<Box p={6} border borderColor="zinc-800" bg="zinc-900/30">
<LeagueActivityFeed leagueId={viewData.leagueId} limit={5} />
</Box>
</Stack>
{/* About the League */}
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">About the League</Text>
<Box p={6} border borderColor="zinc-800" bg="zinc-900/30">
@@ -43,6 +83,7 @@ export function LeagueOverviewTemplate({ viewData }: LeagueOverviewTemplateProps
</Box>
</Stack>
{/* Quick Stats */}
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Quick Stats</Text>
<Box display="grid" responsiveGridCols={{ base: 2, md: 4 }} gap={4}>
@@ -97,6 +138,20 @@ export function LeagueOverviewTemplate({ viewData }: LeagueOverviewTemplateProps
{/* Sidebar */}
<Box as="aside">
<Stack gap={8}>
{/* Admin Quick-View Widgets */}
{isOwnerOrAdmin && (
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Admin Tools</Text>
<AdminQuickViewWidgets
leagueId={viewData.leagueId}
walletBalance={viewData.walletBalance}
pendingProtestsCount={viewData.pendingProtestsCount}
pendingJoinRequestsCount={viewData.pendingJoinRequestsCount}
isOwnerOrAdmin={isOwnerOrAdmin}
/>
</Stack>
)}
<Stack gap={4}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Management</Text>
<Box p={6} border borderColor="zinc-800" bg="zinc-900/30">

View File

@@ -1,35 +1,143 @@
'use client';
import { LeagueSchedulePanel } from '@/components/leagues/LeagueSchedulePanel';
import { useState } from 'react';
import { EnhancedLeagueSchedulePanel } from '@/components/leagues/EnhancedLeagueSchedulePanel';
import { RaceDetailModal } from '@/components/leagues/RaceDetailModal';
import type { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Group } from '@/ui/Group';
import { Calendar, Plus } from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface LeagueScheduleTemplateProps {
viewData: LeagueScheduleViewData;
onRegister: (raceId: string) => Promise<void>;
onWithdraw: (raceId: string) => Promise<void>;
onEdit: (raceId: string) => void;
onReschedule: (raceId: string) => void;
onResultsClick: (raceId: string) => void;
onCreateRace?: () => void;
}
export function LeagueScheduleTemplate({ viewData }: LeagueScheduleTemplateProps) {
export function LeagueScheduleTemplate({
viewData,
onRegister,
onWithdraw,
onEdit,
onReschedule,
onResultsClick,
onCreateRace
}: LeagueScheduleTemplateProps) {
const [selectedRace, setSelectedRace] = useState<{
id: string;
name: string;
track?: string;
car?: string;
sessionType?: string;
scheduledAt: string;
status: 'scheduled' | 'completed';
strengthOfField?: number;
isUserRegistered?: boolean;
canRegister?: boolean;
} | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const events = viewData.races.map(race => ({
id: race.id,
title: race.name || `Race ${race.id.substring(0, 4)}`,
trackName: race.track || 'TBA',
date: race.scheduledAt,
time: DateDisplay.formatDateTime(race.scheduledAt),
status: (race.status === 'completed' ? 'completed' : 'upcoming') as any,
strengthOfField: race.strengthOfField
name: race.name || `Race ${race.id.substring(0, 4)}`,
track: race.track || 'TBA',
car: race.car,
sessionType: race.sessionType,
scheduledAt: race.scheduledAt,
status: race.status,
strengthOfField: race.strengthOfField,
isUserRegistered: race.isUserRegistered,
canRegister: race.canRegister,
canEdit: race.canEdit,
canReschedule: race.canReschedule,
}));
const handleRaceDetail = (raceId: string) => {
const race = viewData.races.find(r => r.id === raceId);
if (race) {
setSelectedRace({
id: race.id,
name: race.name || `Race ${race.id.substring(0, 4)}`,
track: race.track,
car: race.car,
sessionType: race.sessionType,
scheduledAt: race.scheduledAt,
status: race.status,
strengthOfField: race.strengthOfField,
isUserRegistered: race.isUserRegistered,
canRegister: race.canRegister,
});
setModalOpen(true);
}
};
const handleCloseModal = () => {
setModalOpen(false);
setSelectedRace(null);
};
const handleRegister = async (raceId: string) => {
await onRegister(raceId);
setModalOpen(false);
};
const handleWithdraw = async (raceId: string) => {
await onWithdraw(raceId);
setModalOpen(false);
};
return (
<Box display="flex" flexDirection="col" gap={8}>
<Box as="header" display="flex" flexDirection="col" gap={2}>
<Text as="h2" size="xl" weight="bold" color="text-white" uppercase letterSpacing="tight">Race Schedule</Text>
<Group gap={3} align="center">
<Text as="h2" size="xl" weight="bold" color="text-white" uppercase letterSpacing="tight">
Race Schedule
</Text>
{viewData.isAdmin && onCreateRace && (
<Button
variant="primary"
size="sm"
onClick={onCreateRace}
icon={<Icon icon={Plus} size={3} />}
>
Add Race
</Button>
)}
</Group>
<Text size="sm" color="text-zinc-500">Upcoming and past events for this season.</Text>
</Box>
<LeagueSchedulePanel events={events} />
<EnhancedLeagueSchedulePanel
events={events}
leagueId={viewData.leagueId}
currentDriverId={viewData.currentDriverId}
isAdmin={viewData.isAdmin}
onRegister={handleRegister}
onWithdraw={handleWithdraw}
onEdit={onEdit}
onReschedule={onReschedule}
onRaceDetail={handleRaceDetail}
onResultsClick={onResultsClick}
/>
{selectedRace && (
<RaceDetailModal
race={selectedRace}
isOpen={modalOpen}
onClose={handleCloseModal}
onRegister={() => handleRegister(selectedRace.id)}
onWithdraw={() => handleWithdraw(selectedRace.id)}
onResultsClick={() => onResultsClick(selectedRace.id)}
/>
)}
</Box>
);
}

View File

@@ -1,19 +1,29 @@
'use client';
import { useState } from 'react';
import { LeagueStandingsTable } from '@/components/leagues/LeagueStandingsTable';
import type { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { Trophy, Users, Calendar, Award } from 'lucide-react';
interface LeagueStandingsTemplateProps {
viewData: LeagueStandingsViewData;
loading?: boolean;
onToggleTeamChampionship?: () => void;
}
export function LeagueStandingsTemplate({
viewData,
loading = false,
onToggleTeamChampionship,
}: LeagueStandingsTemplateProps) {
const [showTeamStandings, setShowTeamStandings] = useState(false);
if (loading) {
return (
<Box display="flex" alignItems="center" justifyContent="center" py={24}>
@@ -31,21 +41,89 @@ export function LeagueStandingsTemplate({
driverName: driver?.name || 'Unknown Driver',
driverId: entry.driverId,
points: entry.totalPoints,
wins: 0, // Placeholder
podiums: 0, // Placeholder
wins: entry.wins,
podiums: entry.podiums,
races: entry.racesStarted,
avgFinish: entry.avgFinish,
gap: entry.position === 1 ? '—' : `-${viewData.standings[0].totalPoints - entry.totalPoints}`
gap: entry.position === 1 ? '—' : `-${viewData.standings[0].totalPoints - entry.totalPoints}`,
positionChange: entry.positionChange,
lastRacePoints: entry.lastRacePoints,
droppedRaceIds: entry.droppedRaceIds,
};
});
// Calculate championship stats
const championshipStats = {
totalRaces: viewData.standings[0]?.racesStarted || 0,
totalDrivers: viewData.standings.length,
topWins: Math.max(...viewData.standings.map(s => s.wins)),
topPodiums: Math.max(...viewData.standings.map(s => s.podiums)),
};
return (
<Box display="flex" flexDirection="col" gap={8}>
<Box as="header" display="flex" flexDirection="col" gap={2}>
<Text as="h2" size="xl" weight="bold" color="text-white" uppercase letterSpacing="tight">Championship Standings</Text>
<Group gap={3} align="center">
<Text as="h2" size="xl" weight="bold" color="text-white" uppercase letterSpacing="tight">
Championship Standings
</Text>
{viewData.isTeamChampionship && onToggleTeamChampionship && (
<Button
variant="secondary"
size="sm"
onClick={() => {
setShowTeamStandings(!showTeamStandings);
onToggleTeamChampionship();
}}
icon={<Icon icon={Users} size={3} />}
>
{showTeamStandings ? 'Show Driver Standings' : 'Show Team Standings'}
</Button>
)}
</Group>
<Text size="sm" color="text-zinc-500">Official points classification for the current season.</Text>
</Box>
{/* Championship Stats */}
<Box display="flex" gap={4} flexWrap="wrap">
<Surface border borderColor="border-outline-steel" p={4} flex={1} minWidth="200px">
<Group gap={2} align="center">
<Icon icon={Trophy} size={4} color="text-primary-blue" />
<Box>
<Text size="xs" color="text-zinc-500" uppercase letterSpacing="widest">Total Races</Text>
<Text size="lg" weight="bold" color="text-white">{championshipStats.totalRaces}</Text>
</Box>
</Group>
</Surface>
<Surface border borderColor="border-outline-steel" p={4} flex={1} minWidth="200px">
<Group gap={2} align="center">
<Icon icon={Users} size={4} color="text-primary-blue" />
<Box>
<Text size="xs" color="text-zinc-500" uppercase letterSpacing="widest">Total Drivers</Text>
<Text size="lg" weight="bold" color="text-white">{championshipStats.totalDrivers}</Text>
</Box>
</Group>
</Surface>
<Surface border borderColor="border-outline-steel" p={4} flex={1} minWidth="200px">
<Group gap={2} align="center">
<Icon icon={Award} size={4} color="text-primary-blue" />
<Box>
<Text size="xs" color="text-zinc-500" uppercase letterSpacing="widest">Most Wins</Text>
<Text size="lg" weight="bold" color="text-white">{championshipStats.topWins}</Text>
</Box>
</Group>
</Surface>
<Surface border borderColor="border-outline-steel" p={4} flex={1} minWidth="200px">
<Group gap={2} align="center">
<Icon icon={Calendar} size={4} color="text-primary-blue" />
<Box>
<Text size="xs" color="text-zinc-500" uppercase letterSpacing="widest">Most Podiums</Text>
<Text size="lg" weight="bold" color="text-white">{championshipStats.topPodiums}</Text>
</Box>
</Group>
</Surface>
</Box>
<LeagueStandingsTable standings={standings} />
</Box>
);