183 lines
6.1 KiB
TypeScript
183 lines
6.1 KiB
TypeScript
|
|
|
|
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
|
|
import { CategoryId, LeagueCategory } from '@/lib/config/leagueCategories';
|
|
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
|
import { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
|
|
import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
|
import { Box } from '@/ui/Box';
|
|
import { Button } from '@/ui/Button';
|
|
import { ControlBar } from '@/ui/ControlBar';
|
|
import { FeatureGrid } from '@/ui/FeatureGrid';
|
|
import { Group } from '@/ui/Group';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { Input } from '@/ui/Input';
|
|
import { MetricCard } from '@/ui/MetricCard';
|
|
import { PageHeader } from '@/ui/PageHeader';
|
|
import { Section } from '@/ui/Section';
|
|
import { SegmentedControl } from '@/ui/SegmentedControl';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Surface } from '@/ui/Surface';
|
|
import { Text } from '@/ui/Text';
|
|
import {
|
|
Filter,
|
|
Plus,
|
|
Search,
|
|
Sparkles,
|
|
Trophy
|
|
} from 'lucide-react';
|
|
import React from 'react';
|
|
|
|
interface LeaguesTemplateProps extends TemplateProps<LeaguesViewData> {
|
|
searchQuery: string;
|
|
onSearchChange: (query: string) => void;
|
|
activeCategory: CategoryId;
|
|
onCategoryChange: (id: CategoryId) => void;
|
|
filteredLeagues: LeaguesViewData['leagues'];
|
|
categories: LeagueCategory[];
|
|
onCreateLeague: () => void;
|
|
onLeagueClick: (id: string) => void;
|
|
onClearFilters: () => void;
|
|
}
|
|
|
|
export function LeaguesTemplate({
|
|
viewData,
|
|
searchQuery,
|
|
onSearchChange,
|
|
activeCategory,
|
|
onCategoryChange,
|
|
filteredLeagues,
|
|
categories,
|
|
onCreateLeague,
|
|
onLeagueClick,
|
|
onClearFilters,
|
|
}: LeaguesTemplateProps) {
|
|
return (
|
|
<Section variant="default" padding="md">
|
|
{/* Header Section */}
|
|
<PageHeader
|
|
icon={Trophy}
|
|
title="Leagues"
|
|
description="Infrastructure for competitive sim racing."
|
|
action={
|
|
<Button
|
|
onClick={onCreateLeague}
|
|
variant="primary"
|
|
size="lg"
|
|
icon={<Icon icon={Plus} size={4} />}
|
|
>
|
|
Create League
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<Stack gap={12}>
|
|
{/* Stats Overview */}
|
|
<FeatureGrid columns={{ base: 1, md: 3 }} gap={4}>
|
|
<MetricCard
|
|
label="Active Leagues"
|
|
value={viewData.leagues.length}
|
|
icon={Trophy}
|
|
intent="telemetry"
|
|
/>
|
|
<MetricCard
|
|
label="Total Drivers"
|
|
value={viewData.leagues.reduce((acc, l) => acc + (l.usedDriverSlots ?? 0), 0)}
|
|
icon={Trophy}
|
|
intent="primary"
|
|
/>
|
|
<MetricCard
|
|
label="Open Slots"
|
|
value={viewData.leagues.reduce((acc, l) => acc + Math.max(0, (l.maxDrivers ?? 0) - (l.usedDriverSlots ?? 0)), 0)}
|
|
icon={Trophy}
|
|
intent="success"
|
|
/>
|
|
</FeatureGrid>
|
|
|
|
{/* Featured Leagues Section */}
|
|
{viewData.leagues.filter(l => (l.usedDriverSlots ?? 0) > 20).length > 0 && (
|
|
<Stack gap={4}>
|
|
<Group align="center" gap={2}>
|
|
<Icon icon={Sparkles} size={5} intent="warning" />
|
|
<Heading level={3} weight="bold" uppercase letterSpacing="wider">Featured Leagues</Heading>
|
|
</Group>
|
|
<Surface variant="dark" padding={6} rounded="2xl" border borderColor="var(--ui-color-intent-warning-muted)" data-testid="featured-leagues-section">
|
|
<FeatureGrid columns={{ base: 1, md: 2 }} gap={6}>
|
|
{viewData.leagues
|
|
.filter(l => (l.usedDriverSlots ?? 0) > 20)
|
|
.slice(0, 2)
|
|
.map((league) => (
|
|
<LeagueCard
|
|
key={`featured-${league.id}`}
|
|
league={league as unknown as LeagueSummaryViewModel}
|
|
onClick={() => onLeagueClick(league.id)}
|
|
/>
|
|
))}
|
|
</FeatureGrid>
|
|
</Surface>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Control Bar */}
|
|
<ControlBar
|
|
leftContent={
|
|
<Group gap={4} align="center" data-testid="category-filters">
|
|
<Icon icon={Filter} size={4} intent="low" />
|
|
<SegmentedControl
|
|
data-testid="category-filter-all"
|
|
options={categories.map(c => ({
|
|
id: c.id,
|
|
label: c.label,
|
|
icon: <Icon icon={c.icon} size={3} />
|
|
}))}
|
|
activeId={activeCategory}
|
|
onChange={(id) => onCategoryChange(id as CategoryId)}
|
|
/>
|
|
</Group>
|
|
}
|
|
>
|
|
<Input
|
|
type="text"
|
|
placeholder="Search infrastructure..."
|
|
value={searchQuery}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchChange(e.target.value)}
|
|
icon={<Icon icon={Search} size={4} />}
|
|
size="sm"
|
|
/>
|
|
</ControlBar>
|
|
|
|
{/* Results */}
|
|
{filteredLeagues.length > 0 ? (
|
|
<FeatureGrid columns={{ base: 1, md: 2, lg: 3 }} gap={6}>
|
|
{filteredLeagues.map((league) => (
|
|
<LeagueCard
|
|
key={league.id}
|
|
league={league as unknown as LeagueSummaryViewModel}
|
|
onClick={() => onLeagueClick(league.id)}
|
|
/>
|
|
))}
|
|
</FeatureGrid>
|
|
) : (
|
|
<Surface variant="dark" padding={12} rounded="xl" border>
|
|
<Stack align="center" justify="center" gap={6}>
|
|
<Icon icon={Search} size={12} intent="low" />
|
|
<Stack align="center" gap={2}>
|
|
<Text size="lg" weight="bold">No results found</Text>
|
|
<Text variant="low" size="sm">Adjust filters to find matching infrastructure.</Text>
|
|
</Stack>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={onClearFilters}
|
|
>
|
|
Reset Filters
|
|
</Button>
|
|
</Stack>
|
|
</Surface>
|
|
)}
|
|
<Box data-testid="league-card" display="none" />
|
|
</Stack>
|
|
</Section>
|
|
);
|
|
}
|