code quality
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
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
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar';
|
||||
import { LeaderboardTable } from '@/components/leaderboards/LeaderboardTable';
|
||||
import { RankingsPodium } from '@/components/leaderboards/RankingsPodium';
|
||||
@@ -8,13 +6,27 @@ import { Button } from '@/ui/Button';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { PageHeader } from '@/ui/PageHeader';
|
||||
import { ChevronLeft, Trophy } from 'lucide-react';
|
||||
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;
|
||||
}
|
||||
@@ -23,9 +35,37 @@ 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
|
||||
@@ -34,10 +74,11 @@ export function DriverRankingsTemplate({
|
||||
icon={Trophy}
|
||||
action={
|
||||
onBackToLeaderboards && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onBackToLeaderboards}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onBackToLeaderboards}
|
||||
icon={<Icon icon={ChevronLeft} size={4} />}
|
||||
data-testid="back-to-leaderboards"
|
||||
>
|
||||
Back to Leaderboards
|
||||
</Button>
|
||||
@@ -46,7 +87,7 @@ export function DriverRankingsTemplate({
|
||||
/>
|
||||
|
||||
{/* Top 3 Podium */}
|
||||
{viewData.podium.length > 0 && !searchQuery && (
|
||||
{viewData.podium.length > 0 && !searchQuery && currentPage === 1 && (
|
||||
<RankingsPodium
|
||||
podium={viewData.podium.map(d => ({
|
||||
...d,
|
||||
@@ -58,23 +99,90 @@ export function DriverRankingsTemplate({
|
||||
/>
|
||||
)}
|
||||
|
||||
<LeaderboardFiltersBar
|
||||
<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>
|
||||
|
||||
{/* Leaderboard Table */}
|
||||
<LeaderboardTable
|
||||
drivers={viewData.drivers.map(d => ({
|
||||
...d,
|
||||
rating: Number(d.rating),
|
||||
wins: Number(d.wins),
|
||||
racesCompleted: d.racesCompleted || 0,
|
||||
avatarUrl: d.avatarUrl || ''
|
||||
}))}
|
||||
onDriverClick={onDriverClick}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'
|
||||
import { Section } from '@/ui/Section';
|
||||
import { PageHeader } from '@/ui/PageHeader';
|
||||
import { FeatureGrid } from '@/ui/FeatureGrid';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Button } from '@/ui/Button';
|
||||
@@ -54,6 +53,7 @@ export function LeaderboardsTemplate({
|
||||
variant="secondary"
|
||||
onClick={onNavigateToDrivers}
|
||||
icon={<Icon icon={Trophy} size={4} />}
|
||||
data-testid="nav-drivers"
|
||||
>
|
||||
Drivers
|
||||
</Button>
|
||||
@@ -61,6 +61,7 @@ export function LeaderboardsTemplate({
|
||||
variant="secondary"
|
||||
onClick={onNavigateToTeams}
|
||||
icon={<Icon icon={Users} size={4} />}
|
||||
data-testid="nav-teams"
|
||||
>
|
||||
Teams
|
||||
</Button>
|
||||
|
||||
@@ -1,63 +1,220 @@
|
||||
|
||||
|
||||
import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar';
|
||||
import { TeamLeaderboardTable } from '@/components/leaderboards/TeamLeaderboardTable';
|
||||
import type { LeaderboardTeamItem } from '@/lib/view-data/LeaderboardTeamItem';
|
||||
import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { PageHeader } from '@/ui/PageHeader';
|
||||
import { ChevronLeft, Users } from 'lucide-react';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Select } from '@/ui/Select';
|
||||
import { Table, TableBody, TableCell, TableHead, TableRow } from '@/ui/Table';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Award, ChevronLeft, Users, ChevronRight } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
type SkillLevel = 'all' | 'pro' | 'advanced' | 'intermediate' | 'beginner';
|
||||
type SortBy = 'rank' | 'rating' | 'wins' | 'memberCount';
|
||||
|
||||
interface TeamRankingsTemplateProps {
|
||||
viewData: TeamRankingsViewData;
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
onTeamClick?: (id: string) => void;
|
||||
onBackToLeaderboards?: () => void;
|
||||
onSkillChange: (level: SkillLevel) => void;
|
||||
onSortChange: (sort: SortBy) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalTeams: number;
|
||||
onTeamClick: (id: string) => void;
|
||||
onBackToLeaderboards: () => void;
|
||||
}
|
||||
|
||||
export function TeamRankingsTemplate({
|
||||
viewData,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
onSkillChange,
|
||||
onSortChange,
|
||||
onPageChange,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalTeams,
|
||||
onTeamClick,
|
||||
onBackToLeaderboards,
|
||||
}: TeamRankingsTemplateProps): React.ReactElement {
|
||||
}: TeamRankingsTemplateProps) {
|
||||
const { searchQuery, selectedSkill, sortBy, teams } = viewData;
|
||||
|
||||
const levelOptions = [
|
||||
{ value: 'all', label: 'All Levels' },
|
||||
{ value: 'pro', label: 'Professional' },
|
||||
{ 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: 'memberCount', label: 'Members' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Container size="lg" spacing="md">
|
||||
<PageHeader
|
||||
title="Team Leaderboard"
|
||||
description="Global rankings of all teams based on performance and consistency"
|
||||
icon={Users}
|
||||
action={
|
||||
onBackToLeaderboards && (
|
||||
<Section variant="default" padding="lg">
|
||||
<Group direction="column" gap={8} fullWidth>
|
||||
{/* Header */}
|
||||
<Group direction="row" align="center" justify="between" fullWidth>
|
||||
<Group direction="row" align="center" gap={4}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onBackToLeaderboards}
|
||||
icon={<Icon icon={ChevronLeft} size={4} />}
|
||||
data-testid="back-to-leaderboards"
|
||||
>
|
||||
Back to Leaderboards
|
||||
Back
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Group direction="column">
|
||||
<Heading level={1} weight="bold">Team Leaderboard</Heading>
|
||||
<Text variant="low" size="sm" font="mono" uppercase letterSpacing="widest">Global Performance Index</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
<Icon icon={Award} size={8} intent="warning" />
|
||||
</Group>
|
||||
|
||||
<LeaderboardFiltersBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={onSearchChange}
|
||||
placeholder="Search teams..."
|
||||
/>
|
||||
<LeaderboardFiltersBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={onSearchChange}
|
||||
placeholder="Search teams..."
|
||||
>
|
||||
<Group gap={4}>
|
||||
<Select
|
||||
size="sm"
|
||||
value={selectedSkill}
|
||||
options={levelOptions}
|
||||
onChange={(e) => onSkillChange(e.target.value as SkillLevel)}
|
||||
data-testid="skill-filter"
|
||||
/>
|
||||
<Select
|
||||
size="sm"
|
||||
value={sortBy}
|
||||
options={sortOptions}
|
||||
onChange={(e) => onSortChange(e.target.value as SortBy)}
|
||||
data-testid="sort-filter"
|
||||
/>
|
||||
</Group>
|
||||
</LeaderboardFiltersBar>
|
||||
|
||||
<TeamLeaderboardTable
|
||||
teams={viewData.teams.map(t => ({
|
||||
...t,
|
||||
totalRaces: t.totalRaces || 0,
|
||||
rating: t.rating || 0
|
||||
}))}
|
||||
onTeamClick={onTeamClick}
|
||||
/>
|
||||
</Container>
|
||||
<Box paddingY={2}>
|
||||
<Text variant="low" size="sm" data-testid="team-count">
|
||||
Showing {totalTeams} teams
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Panel variant="dark" padding="none">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell w="80px">Rank</TableCell>
|
||||
<TableCell>Team</TableCell>
|
||||
<TableCell textAlign="center">Personnel</TableCell>
|
||||
<TableCell textAlign="center">Races</TableCell>
|
||||
<TableCell textAlign="center">Wins</TableCell>
|
||||
<TableCell textAlign="right">Rating</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{teams.length > 0 ? (
|
||||
teams.map((team) => (
|
||||
<TableRow
|
||||
key={team.id}
|
||||
onClick={() => onTeamClick(team.id)}
|
||||
clickable
|
||||
data-testid={`standing-team-${team.id}`}
|
||||
>
|
||||
<TableCell>
|
||||
<Text font="mono" weight="bold" variant={team.position <= 3 ? 'warning' : 'low'} data-testid={`standing-position-${team.position}`}>
|
||||
#{team.position}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Panel variant="muted" padding="sm">
|
||||
<Icon icon={Users} size={4} intent="low" />
|
||||
</Panel>
|
||||
<Group direction="column" gap={0}>
|
||||
<Text weight="bold" size="sm" data-testid="team-name">{team.name}</Text>
|
||||
<Text size="xs" variant="low" uppercase font="mono">{team.performanceLevel}</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Text size="xs" variant="low" font="mono" data-testid="team-member-count">{team.memberCount}</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Group direction="column" align="center" gap={0} data-testid="standing-stats">
|
||||
<Text size="xs" variant="low" font="mono" data-testid="stat-races">{team.totalRaces}</Text>
|
||||
</Group>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Group direction="column" align="center" gap={0} data-testid="standing-stats">
|
||||
<Text size="xs" variant="low" font="mono" data-testid="stat-wins">{team.totalWins}</Text>
|
||||
</Group>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<Group direction="column" align="end" gap={0} data-testid="standing-stats">
|
||||
<Text font="mono" weight="bold" variant="primary" data-testid="stat-rating">
|
||||
{team.rating?.toFixed(0) || '1000'}
|
||||
</Text>
|
||||
</Group>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} textAlign="center">
|
||||
<Box padding={12} data-testid="empty-state">
|
||||
<Text variant="low" font="mono" size="xs" uppercase letterSpacing="widest">
|
||||
No teams found matching criteria
|
||||
</Text>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Panel>
|
||||
|
||||
{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>
|
||||
)}
|
||||
</Group>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user