Some checks failed
CI / lint-typecheck (pull_request) Failing after 13s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
189 lines
5.9 KiB
TypeScript
189 lines
5.9 KiB
TypeScript
import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar';
|
|
import { LeaderboardTable } from '@/components/leaderboards/LeaderboardTable';
|
|
import { RankingsPodium } from '@/components/leaderboards/RankingsPodium';
|
|
import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData';
|
|
import { Button } from '@/ui/Button';
|
|
import { Container } from '@/ui/Container';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { PageHeader } from '@/ui/PageHeader';
|
|
import { Text } from '@/ui/Text';
|
|
import { Box } from '@/ui/Box';
|
|
import { Group } from '@/ui/Group';
|
|
import { Select } from '@/ui/Select';
|
|
import { ChevronLeft, Trophy, ChevronRight } from 'lucide-react';
|
|
import React from 'react';
|
|
|
|
type SkillLevel = 'all' | 'pro' | 'advanced' | 'intermediate' | 'beginner';
|
|
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
|
|
|
|
interface DriverRankingsTemplateProps {
|
|
viewData: DriverRankingsViewData;
|
|
searchQuery: string;
|
|
onSearchChange: (query: string) => void;
|
|
onSkillChange: (skill: SkillLevel) => void;
|
|
onTeamChange: (teamId: string) => void;
|
|
onSortChange: (sort: SortBy) => void;
|
|
onPageChange: (page: number) => void;
|
|
currentPage: number;
|
|
totalPages: number;
|
|
totalDrivers: number;
|
|
onDriverClick?: (id: string) => void;
|
|
onBackToLeaderboards?: () => void;
|
|
}
|
|
|
|
export function DriverRankingsTemplate({
|
|
viewData,
|
|
searchQuery,
|
|
onSearchChange,
|
|
onSkillChange,
|
|
onTeamChange,
|
|
onSortChange,
|
|
onPageChange,
|
|
currentPage,
|
|
totalPages,
|
|
totalDrivers,
|
|
onDriverClick,
|
|
onBackToLeaderboards,
|
|
}: DriverRankingsTemplateProps): React.ReactElement {
|
|
const skillOptions = [
|
|
{ value: 'all', label: 'All Skills' },
|
|
{ value: 'pro', label: 'Pro' },
|
|
{ value: 'advanced', label: 'Advanced' },
|
|
{ value: 'intermediate', label: 'Intermediate' },
|
|
{ value: 'beginner', label: 'Beginner' },
|
|
];
|
|
|
|
const sortOptions = [
|
|
{ value: 'rank', label: 'Rank' },
|
|
{ value: 'rating', label: 'Rating' },
|
|
{ value: 'wins', label: 'Wins' },
|
|
{ value: 'podiums', label: 'Podiums' },
|
|
{ value: 'winRate', label: 'Win Rate' },
|
|
];
|
|
|
|
const teamOptions = [
|
|
{ value: 'all', label: 'All Teams' },
|
|
...viewData.availableTeams.map(t => ({ value: t.id, label: t.name })),
|
|
];
|
|
|
|
return (
|
|
<Container size="lg" spacing="md">
|
|
<PageHeader
|
|
title="Driver Leaderboard"
|
|
description="Full rankings of all drivers by performance metrics"
|
|
icon={Trophy}
|
|
action={
|
|
onBackToLeaderboards && (
|
|
<Button
|
|
variant="secondary"
|
|
onClick={onBackToLeaderboards}
|
|
icon={<Icon icon={ChevronLeft} size={4} />}
|
|
data-testid="back-to-leaderboards"
|
|
>
|
|
Back to Leaderboards
|
|
</Button>
|
|
)
|
|
}
|
|
/>
|
|
|
|
{/* Top 3 Podium */}
|
|
{viewData.podium.length > 0 && !searchQuery && currentPage === 1 && (
|
|
<RankingsPodium
|
|
podium={viewData.podium.map(d => ({
|
|
...d,
|
|
rating: Number(d.rating),
|
|
wins: Number(d.wins),
|
|
podiums: Number(d.podiums)
|
|
}))}
|
|
onDriverClick={onDriverClick}
|
|
/>
|
|
)}
|
|
|
|
<LeaderboardFiltersBar
|
|
searchQuery={searchQuery}
|
|
onSearchChange={onSearchChange}
|
|
placeholder="Search drivers..."
|
|
>
|
|
<Group gap={2}>
|
|
<Select
|
|
size="sm"
|
|
value={viewData.selectedSkill}
|
|
options={skillOptions}
|
|
onChange={(e) => onSkillChange(e.target.value as SkillLevel)}
|
|
data-testid="skill-filter"
|
|
/>
|
|
<Select
|
|
size="sm"
|
|
value={viewData.selectedTeam}
|
|
options={teamOptions}
|
|
onChange={(e) => onTeamChange(e.target.value)}
|
|
data-testid="team-filter"
|
|
/>
|
|
<Select
|
|
size="sm"
|
|
value={viewData.sortBy}
|
|
options={sortOptions}
|
|
onChange={(e) => onSortChange(e.target.value as SortBy)}
|
|
data-testid="sort-filter"
|
|
/>
|
|
</Group>
|
|
</LeaderboardFiltersBar>
|
|
|
|
<Box paddingY={2}>
|
|
<Text variant="low" size="sm" data-testid="driver-count">
|
|
Showing {totalDrivers} drivers
|
|
</Text>
|
|
</Box>
|
|
|
|
{viewData.drivers.length === 0 ? (
|
|
<Box paddingY={12} textAlign="center" data-testid="empty-state">
|
|
<Text variant="low">{searchQuery ? `No drivers found matching "${searchQuery}"` : 'No drivers available'}</Text>
|
|
</Box>
|
|
) : (
|
|
<>
|
|
<LeaderboardTable
|
|
drivers={viewData.drivers.map(d => ({
|
|
...d,
|
|
rating: Number(d.rating),
|
|
wins: Number(d.wins),
|
|
racesCompleted: d.racesCompleted || 0,
|
|
avatarUrl: d.avatarUrl || ''
|
|
}))}
|
|
onDriverClick={onDriverClick}
|
|
/>
|
|
|
|
{totalPages > 1 && (
|
|
<Box paddingY={8}>
|
|
<Group justify="center" gap={4} data-testid="pagination-controls">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={currentPage === 1}
|
|
onClick={() => onPageChange(currentPage - 1)}
|
|
icon={<Icon icon={ChevronLeft} size={4} />}
|
|
data-testid="prev-page"
|
|
>
|
|
Previous
|
|
</Button>
|
|
<Text variant="low" size="sm" font="mono">
|
|
Page {currentPage} of {totalPages}
|
|
</Text>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={currentPage === totalPages}
|
|
onClick={() => onPageChange(currentPage + 1)}
|
|
icon={<Icon icon={ChevronRight} size={4} />}
|
|
data-testid="next-page"
|
|
>
|
|
Next
|
|
</Button>
|
|
</Group>
|
|
</Box>
|
|
)}
|
|
</>
|
|
)}
|
|
</Container>
|
|
);
|
|
}
|