website refactor

This commit is contained in:
2026-01-18 13:26:35 +01:00
parent 350c78504d
commit 0b301feb61
225 changed files with 1678 additions and 26666 deletions

View File

@@ -8,8 +8,8 @@ import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { RecruitingTeamGrid } from '@/ui/RecruitingTeamGrid';
import { RecruitingTeamCard } from '@/ui/RecruitingTeamCard';
import { RecruitingTeamGrid } from '@/components/teams/RecruitingTeamGrid';
import { RecruitingTeamCard } from '@/components/teams/RecruitingTeamCard';
interface FeaturedRecruitingProps {
teams: Array<{

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Users, Trophy } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
interface RecruitingTeamCardProps {
name: string;
description?: string;
logoUrl: string;
category?: string;
memberCount: number;
totalWins: number;
onClick: () => void;
}
export function RecruitingTeamCard({
name,
description,
logoUrl,
category,
memberCount,
totalWins,
onClick,
}: RecruitingTeamCardProps) {
return (
<Box
as="button"
type="button"
onClick={onClick}
p={4}
rounded="xl"
bg="bg-iron-gray/60"
border={true}
borderColor="border-charcoal-outline"
className="hover:border-performance-green/40 transition-all duration-200 text-left group"
>
<Box display="flex" alignItems="start" justifyContent="between" mb={3}>
<Box width="8" height="8" rounded="lg" bg="bg-charcoal-outline" border={true} borderColor="border-charcoal-outline" overflow="hidden">
<Image
src={logoUrl}
alt={name}
width={32}
height={32}
objectFit="cover"
/>
</Box>
<Badge variant="success">
<Box w="1.5" h="1.5" rounded="full" bg="bg-performance-green" animate="pulse" mr={1} />
Recruiting
</Badge>
</Box>
<Text color="text-white" weight="semibold" block mb={1} className="group-hover:text-performance-green transition-colors line-clamp-1">
{name}
</Text>
<Text size="xs" color="text-gray-500" block mb={3} className="line-clamp-2">{description}</Text>
<Stack direction="row" align="center" gap={2} wrap>
{category && (
<Stack direction="row" align="center" gap={1}>
<Box w="1.5" h="1.5" rounded="full" bg="bg-purple-400" />
<Text size="xs" color="text-purple-400">{category}</Text>
</Stack>
)}
<Stack direction="row" align="center" gap={1}>
<Icon icon={Users} size={3} color="text-gray-400" />
<Text size="xs" color="text-gray-400">{memberCount}</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Trophy} size={3} color="text-gray-400" />
<Text size="xs" color="text-gray-400">{totalWins} wins</Text>
</Stack>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import React, { ReactNode } from 'react';
import { Box } from '@/ui/Box';
interface RecruitingTeamGridProps {
children: ReactNode;
}
export function RecruitingTeamGrid({ children }: RecruitingTeamGridProps) {
return (
<Box display="grid" gridCols={{ base: 1, md: 2, lg: 4 }} gap={4}>
{children}
</Box>
);
}

View File

@@ -2,9 +2,9 @@
import React, { useState } from 'react';
import { LucideIcon } from 'lucide-react';
import { TeamCard } from '@/ui/TeamCardWrapper';
import { SkillLevelHeader } from '@/ui/SkillLevelHeader';
import { TeamGrid } from '@/ui/TeamGrid';
import { TeamCard } from '@/components/teams/TeamCardWrapper';
import { SkillLevelHeader } from '@/components/drivers/SkillLevelHeader';
import { TeamGrid } from '@/components/teams/TeamGrid';
import { Box } from '@/ui/Box';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';

View File

@@ -9,8 +9,8 @@ import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { JoinRequestList } from '@/ui/JoinRequestList';
import { JoinRequestItem } from '@/ui/JoinRequestItem';
import { JoinRequestList } from '@/components/leagues/JoinRequestList';
import { JoinRequestItem } from '@/components/leagues/JoinRequestItem';
import { DangerZone } from '@/ui/DangerZone';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
import { useTeamJoinRequests } from "@/hooks/team/useTeamJoinRequests";

View File

@@ -1,83 +1,173 @@
'use client';
import React from 'react';
import { Users, ChevronRight } from 'lucide-react';
import {
ChevronRight,
Globe,
Users
} from 'lucide-react';
import { ReactNode } from 'react';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface TeamCardProps {
id: string;
name: string;
logoUrl?: string;
description?: string;
logo?: string;
memberCount: number;
isRecruiting?: boolean;
performanceBadge?: ReactNode;
specializationContent?: ReactNode;
categoryBadge?: ReactNode;
region?: string;
languagesContent?: ReactNode;
statsContent?: ReactNode;
onClick?: () => void;
}
export function TeamCard({ name, logoUrl, memberCount, isRecruiting, onClick }: TeamCardProps) {
export function TeamCard({
name,
description,
logo,
memberCount,
isRecruiting,
performanceBadge,
specializationContent,
categoryBadge,
region,
languagesContent,
statsContent: _statsContent,
onClick,
}: TeamCardProps) {
return (
<Box
as="article"
bg="surface-charcoal"
border
borderColor="outline-steel"
p={5}
cursor="pointer"
hover={{ borderColor: 'primary-accent', bg: 'surface-charcoal/80' }}
transition="smooth"
onClick={onClick}
position="relative"
overflow="hidden"
>
{/* Accent line */}
<Box
position="absolute"
top="0"
left="0"
w="1"
h="full"
bg={isRecruiting ? 'telemetry-aqua' : 'outline-steel'}
/>
<Box onClick={onClick} h="full" cursor={onClick ? 'pointer' : 'default'} className="group">
<Card h="full" p={0} display="flex" flexDirection="col" overflow="hidden" className="bg-panel-gray/40 border-border-gray/50 hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300">
{/* Header with Logo */}
<Box p={5} pb={0}>
<Stack direction="row" align="start" gap={4}>
{/* Logo */}
<Box
w="16"
h="16"
rounded="none"
bg="graphite-black"
display="flex"
center
overflow="hidden"
border
borderColor="border-gray/50"
className="relative"
>
{logo ? (
<Image
src={logo}
alt={name}
width={64}
height={64}
fullWidth
fullHeight
objectFit="cover"
/>
) : (
<PlaceholderImage size={64} />
)}
<Box position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="primary-accent/30" />
</Box>
<Stack direction="row" align="center" gap={4}>
<Box
w="12"
h="12"
bg="base-black"
border
borderColor="outline-steel"
display="flex"
center
overflow="hidden"
>
{logoUrl ? (
<Image src={logoUrl} alt={name} width={48} height={48} />
) : (
<Text size="xs" weight="bold" color="text-gray-600">{name.substring(0, 2).toUpperCase()}</Text>
)}
</Box>
<Box flex="1">
<Heading level={4} weight="bold">{name}</Heading>
<Stack direction="row" align="center" gap={3} mt={1}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Users} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500" font="mono">{memberCount}</Text>
</Stack>
{isRecruiting && (
<Box px={1.5} py={0.5} bg="telemetry-aqua/10" border borderColor="telemetry-aqua/20">
<Text size="xs" color="telemetry-aqua" weight="bold" uppercase letterSpacing="tighter">Recruiting</Text>
</Box>
)}
{/* Title & Badges */}
<Box flexGrow={1} minWidth="0">
<Stack direction="row" align="start" justify="between" gap={2}>
<Heading level={4} weight="bold" fontSize="lg" className="tracking-tight group-hover:text-primary-accent transition-colors">
{name}
</Heading>
{isRecruiting && (
<Badge variant="success" size="xs">
RECRUITING
</Badge>
)}
</Stack>
{/* Performance Level & Category */}
<Stack direction="row" align="center" gap={2} wrap mt={2}>
{performanceBadge}
{specializationContent}
{categoryBadge}
</Stack>
</Box>
</Stack>
</Box>
<Icon icon={ChevronRight} size={4} color="text-gray-700" />
</Stack>
{/* Content */}
<Box p={5} display="flex" flexDirection="col" flexGrow={1}>
{/* Description */}
<Text
size="xs"
color="text-gray-500"
mb={4}
lineClamp={2}
block
leading="relaxed"
style={{ height: '2.5rem' }}
>
{description || 'No description available'}
</Text>
{/* Region & Languages */}
{(region || languagesContent) && (
<Stack direction="row" align="center" gap={2} wrap mb={4}>
{region && (
<Box
display="flex"
alignItems="center"
gap={2}
px={2}
py={1}
rounded="none"
bg="panel-gray/20"
border
borderColor="border-gray/30"
>
<Icon icon={Globe} size={3} color="text-primary-accent" />
<Text size="xs" color="text-gray-400" weight="bold" className="uppercase tracking-widest">{region}</Text>
</Box>
)}
{languagesContent}
</Stack>
)}
{/* Spacer */}
<Box flexGrow={1} />
{/* Footer */}
<Box
display="flex"
alignItems="center"
justifyContent="between"
pt={4}
borderTop
borderColor="border-gray/30"
mt="auto"
>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Users} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500" font="mono">
{memberCount} {memberCount === 1 ? 'MEMBER' : 'MEMBERS'}
</Text>
</Stack>
<Stack direction="row" align="center" gap={1} className="group-hover:text-primary-accent transition-colors">
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">VIEW</Text>
<Icon icon={ChevronRight} size={3} color="text-gray-500" className="transition-transform group-hover:translate-x-0.5" />
</Stack>
</Box>
</Box>
</Card>
</Box>
);
}

View File

@@ -0,0 +1,137 @@
import React from 'react';
import {
Zap,
Clock,
Languages,
Crown,
Star,
TrendingUp,
Shield
} from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { TeamCard as UiTeamCard } from '@/components/teams/TeamCard';
import { Icon } from '@/ui/Icon';
import { TeamStatItem } from '@/components/teams/TeamStatItem';
interface TeamCardProps {
id: string;
name: string;
description?: string;
logo?: string;
memberCount: number;
rating?: number | null;
totalWins?: number;
totalRaces?: number;
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
isRecruiting?: boolean;
specialization?: 'endurance' | 'sprint' | 'mixed' | undefined;
region?: string;
languages?: string[] | undefined;
leagues?: string[];
category?: string;
onClick?: () => void;
}
function getPerformanceBadge(level?: string) {
switch (level) {
case 'pro':
return { icon: Crown, label: 'Pro', variant: 'warning' as const };
case 'advanced':
return { icon: Star, label: 'Advanced', variant: 'primary' as const };
case 'intermediate':
return { icon: TrendingUp, label: 'Intermediate', variant: 'info' as const };
case 'beginner':
return { icon: Shield, label: 'Beginner', variant: 'success' as const };
default:
return null;
}
}
function getSpecializationBadge(specialization?: string) {
switch (specialization) {
case 'endurance':
return { icon: Clock, label: 'Endurance', color: 'var(--warning-amber)' };
case 'sprint':
return { icon: Zap, label: 'Sprint', color: 'var(--neon-aqua)' };
default:
return null;
}
}
export function TeamCard({
name,
description,
logo,
memberCount,
rating,
totalWins,
totalRaces,
performanceLevel,
isRecruiting,
specialization,
region,
languages,
category,
onClick,
}: TeamCardProps) {
const performanceBadge = getPerformanceBadge(performanceLevel);
const specializationBadge = getSpecializationBadge(specialization);
return (
<UiTeamCard
name={name}
description={description}
logo={logo}
memberCount={memberCount}
isRecruiting={isRecruiting}
onClick={onClick}
region={region}
performanceBadge={performanceBadge && (
<Badge variant={performanceBadge.variant} icon={performanceBadge.icon}>
{performanceBadge.label}
</Badge>
)}
specializationContent={specializationBadge && (
<Stack direction="row" align="center" gap={1}>
<Icon icon={specializationBadge.icon} size={3} color={specializationBadge.color} />
<Text size="xs" color="text-gray-500">{specializationBadge.label}</Text>
</Stack>
)}
categoryBadge={category && (
<Badge variant="primary">
<Box w="2" h="2" rounded="full" bg="bg-purple-500" mr={1.5} />
{category}
</Badge>
)}
languagesContent={languages && languages.length > 0 && (
<Box
display="flex"
alignItems="center"
gap={1.5}
px={2}
py={1}
rounded="md"
bg="bg-iron-gray/50"
border
borderColor="border-charcoal-outline/30"
>
<Icon icon={Languages} size={3} color="var(--neon-purple)" />
<Text size="xs" color="text-gray-400">
{languages.slice(0, 2).join(', ')}
{languages.length > 2 && ` +${languages.length - 2}`}
</Text>
</Box>
)}
statsContent={
<>
<TeamStatItem label="Rating" value={typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'} color="text-primary-blue" align="center" />
<TeamStatItem label="Wins" value={totalWins ?? 0} color="text-performance-green" align="center" />
<TeamStatItem label="Races" value={totalRaces ?? 0} color="text-white" align="center" />
</>
}
/>
);
}

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { Search, Star, Trophy, Percent, Hash, LucideIcon } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
const SKILL_LEVELS: {
id: SkillLevel;
label: string;
variant: 'warning' | 'primary' | 'info' | 'success';
}[] = [
{ id: 'pro', label: 'Pro', variant: 'warning' },
{ id: 'advanced', label: 'Advanced', variant: 'primary' },
{ id: 'intermediate', label: 'Intermediate', variant: 'info' },
{ id: 'beginner', label: 'Beginner', variant: 'success' },
];
const SORT_OPTIONS: { id: SortBy; label: string; icon: LucideIcon }[] = [
{ id: 'rating', label: 'Rating', icon: Star },
{ id: 'wins', label: 'Total Wins', icon: Trophy },
{ id: 'winRate', label: 'Win Rate', icon: Percent },
{ id: 'races', label: 'Races', icon: Hash },
];
interface TeamFilterProps {
searchQuery: string;
onSearchChange: (query: string) => void;
filterLevel: SkillLevel | 'all';
onFilterLevelChange: (level: SkillLevel | 'all') => void;
sortBy: SortBy;
onSortChange: (sort: SortBy) => void;
}
export function TeamFilter({
searchQuery,
onSearchChange,
filterLevel,
onFilterLevelChange,
sortBy,
onSortChange,
}: TeamFilterProps) {
return (
<Stack mb={6} gap={4}>
{/* Search and Level Filter Row */}
<Stack direction="row" align="center" gap={4} wrap>
<Box maxWidth="448px" fullWidth>
<Input
type="text"
placeholder="Search teams..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
icon={<Icon icon={Search} size={5} color="text-gray-500" />}
/>
</Box>
{/* Level Filter */}
<Stack direction="row" align="center" gap={2} wrap>
<Button
variant={filterLevel === 'all' ? 'race-final' : 'secondary'}
size="sm"
onClick={() => onFilterLevelChange('all')}
>
All Levels
</Button>
{SKILL_LEVELS.map((level) => {
const isActive = filterLevel === level.id;
return (
<Button
key={level.id}
variant={isActive ? 'primary' : 'secondary'}
size="sm"
onClick={() => onFilterLevelChange(level.id)}
>
{isActive ? (
<Badge variant={level.variant}>{level.label}</Badge>
) : (
level.label
)}
</Button>
);
})}
</Stack>
</Stack>
{/* Sort Options */}
<Stack direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-400">Sort by:</Text>
<Box p={1} rounded="lg" border={true} borderColor="border-charcoal-outline" bg="bg-iron-gray" bgOpacity={0.5}>
<Stack direction="row" align="center" gap={1}>
{SORT_OPTIONS.map((option) => {
const isActive = sortBy === option.id;
return (
<Button
key={option.id}
variant={isActive ? 'race-final' : 'ghost'}
size="sm"
onClick={() => onSortChange(option.id)}
icon={<Icon icon={option.icon} size={3.5} />}
>
{option.label}
</Button>
);
})}
</Stack>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -1,29 +1,14 @@
'use client';
import React from 'react';
import { Grid } from '@/ui/Grid';
import { TeamCard } from './TeamCard';
import type { TeamSummaryData } from '@/lib/view-data/TeamsViewData';
import React, { ReactNode } from 'react';
import { Box } from '@/ui/Box';
interface TeamGridProps {
teams: TeamSummaryData[];
onTeamClick?: (teamId: string) => void;
children: ReactNode;
}
export function TeamGrid({ teams, onTeamClick }: TeamGridProps) {
export function TeamGrid({ children }: TeamGridProps) {
return (
<Grid cols={1} mdCols={2} lgCols={3} gap={6}>
{teams.map((team) => (
<TeamCard
key={team.teamId}
id={team.teamId}
name={team.teamName}
logoUrl={team.logoUrl}
memberCount={team.memberCount}
isRecruiting={true} // Redesign feel
onClick={() => onTeamClick?.(team.teamId)}
/>
))}
</Grid>
<Box display="grid" gridCols={{ base: 1, md: 2, lg: 3 }} gap={4}>
{children}
</Box>
);
}

View File

@@ -0,0 +1,117 @@
import React, { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { TeamLogo } from './TeamLogo';
import { TeamTag } from './TeamTag';
interface TeamHeaderPanelProps {
teamId: string;
name: string;
tag?: string | null;
description?: string;
memberCount: number;
activeLeaguesCount?: number;
foundedDate?: string;
category?: string | null;
actions?: ReactNode;
}
export function TeamHeaderPanel({
teamId,
name,
tag,
description,
memberCount,
activeLeaguesCount,
foundedDate,
category,
actions,
}: TeamHeaderPanelProps) {
return (
<Box
bg="surface-charcoal"
border
borderColor="border-steel-grey"
p={6}
className="relative overflow-hidden"
>
{/* Instrument-grade accent corner */}
<Box position="absolute" top="-1px" left="-1px" w="4" h="4" borderTop borderLeft borderColor="primary-blue/40" />
<Stack direction="row" align="start" justify="between" wrap gap={6}>
<Stack direction="row" align="start" gap={6} wrap flexGrow={1}>
{/* Logo Container */}
<Box
w="24"
h="24"
bg="base-graphite"
border
borderColor="border-steel-grey"
display="flex"
center
overflow="hidden"
className="relative"
>
<TeamLogo teamId={teamId} alt={name} />
{/* Corner detail */}
<Box position="absolute" bottom="0" right="0" w="2" h="2" bg="primary-blue/20" />
</Box>
<Box flexGrow={1} minWidth="0">
<Stack direction="row" align="center" gap={3} mb={2}>
<Heading level={1} weight="bold" className="tracking-tight">{name}</Heading>
{tag && <TeamTag tag={tag} />}
</Stack>
{description && (
<Text color="text-gray-400" block mb={4} maxWidth="42rem" size="sm" leading="relaxed">
{description}
</Text>
)}
<Stack direction="row" align="center" gap={4} wrap>
<Box display="flex" alignItems="center" gap={1.5}>
<Box w="1.5" h="1.5" bg="primary-blue" />
<Text size="xs" color="text-gray-300" font="mono" className="uppercase tracking-wider">
{memberCount} {memberCount === 1 ? 'Member' : 'Members'}
</Text>
</Box>
{category && (
<Box display="flex" alignItems="center" gap={1.5}>
<Box w="1.5" h="1.5" bg="telemetry-aqua" />
<Text size="xs" color="text-gray-300" font="mono" className="uppercase tracking-wider">
{category}
</Text>
</Box>
)}
{activeLeaguesCount !== undefined && (
<Box display="flex" alignItems="center" gap={1.5}>
<Box w="1.5" h="1.5" bg="warning-amber" />
<Text size="xs" color="text-gray-300" font="mono" className="uppercase tracking-wider">
{activeLeaguesCount} {activeLeaguesCount === 1 ? 'League' : 'Leagues'}
</Text>
</Box>
)}
{foundedDate && (
<Text size="xs" color="text-gray-500" font="mono" className="uppercase tracking-wider">
EST. {foundedDate}
</Text>
)}
</Stack>
</Box>
</Stack>
{actions && (
<Box>
{actions}
</Box>
)}
</Stack>
</Box>
);
}

View File

@@ -5,8 +5,8 @@ import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { TeamLogo } from '@/ui/TeamLogo';
import { TeamTag } from '@/ui/TeamTag';
import { TeamLogo } from '@/components/teams/TeamLogo';
import { TeamTag } from '@/components/teams/TeamTag';
import { Text } from '@/ui/Text';
interface TeamHeroProps {

View File

@@ -3,7 +3,7 @@
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { SkillLevelButton } from '@/ui/SkillLevelButton';
import { SkillLevelButton } from '@/components/drivers/SkillLevelButton';
import { Stack } from '@/ui/Stack';
import { TeamHeroSection as UiTeamHeroSection } from '@/components/teams/TeamHeroSection';
import { TeamHeroStats } from '@/components/teams/TeamHeroStats';

View File

@@ -0,0 +1,73 @@
import React from 'react';
import { TableRow, TableCell } from '@/ui/Table';
import { Image } from '@/ui/Image';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
export interface TeamLadderRowProps {
rank: number;
teamId: string;
teamName: string;
logoUrl: string;
memberCount: number;
teamRating: number | null;
totalWins: number;
totalRaces: number;
onClick: () => void;
}
export function TeamLadderRow({
rank,
teamName,
logoUrl,
memberCount,
teamRating,
totalWins,
totalRaces,
onClick,
}: TeamLadderRowProps) {
return (
<TableRow
onClick={onClick}
clickable
>
<TableCell>
<Text size="sm" color="text-gray-300" weight="semibold">#{rank}</Text>
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" gap={3}>
<Box w="8" h="8" rounded="md" overflow="hidden" bg="bg-deep-graphite" flexShrink={0}>
<Image
src={logoUrl}
alt={teamName}
width={32}
height={32}
objectFit="cover"
/>
</Box>
<Box display="flex" flexDirection="col">
<Text size="sm" weight="semibold" color="text-white" truncate block>
{teamName}
</Text>
</Box>
</Box>
</TableCell>
<TableCell>
<Text color="text-primary-blue" weight="semibold">
{teamRating !== null ? Math.round(teamRating) : '—'}
</Text>
</TableCell>
<TableCell>
<Text color="text-performance-green" weight="semibold">{totalWins}</Text>
</TableCell>
<TableCell>
<Text color="text-white">{totalRaces}</Text>
</TableCell>
<TableCell>
<Text color="text-gray-300">
{memberCount} {memberCount === 1 ? 'member' : 'members'}
</Text>
</TableCell>
</TableRow>
);
}

View File

@@ -0,0 +1,26 @@
import React, { ReactNode } from 'react';
import { Table, TableHead, TableBody, TableRow, TableHeader } from '@/ui/Table';
interface TeamLadderTableProps {
children: ReactNode;
}
export function TeamLadderTable({ children }: TeamLadderTableProps) {
return (
<Table>
<TableHead>
<TableRow>
<TableHeader>Rank</TableHeader>
<TableHeader>Team</TableHeader>
<TableHeader>Rating</TableHeader>
<TableHeader>Wins</TableHeader>
<TableHeader>Races</TableHeader>
<TableHeader>Members</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{children}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,127 @@
import { Crown, Trophy, Users } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface TeamLeaderboardItemProps {
position: number;
name: string;
logoUrl: string;
category?: string;
memberCount: number;
totalWins: number;
isRecruiting: boolean;
rating?: number;
onClick?: () => void;
medalColor: string;
medalBg: string;
medalBorder: string;
}
export function TeamLeaderboardItem({
position,
name,
logoUrl,
category,
memberCount,
totalWins,
isRecruiting,
rating,
onClick,
medalColor,
medalBg,
medalBorder,
}: TeamLeaderboardItemProps) {
return (
<Box
as="button"
type="button"
onClick={onClick}
display="flex"
alignItems="center"
gap={4}
px={4}
py={3}
fullWidth
textAlign="left"
bg="transparent"
border="none"
cursor="pointer"
borderBottom
style={{ borderColor: 'rgba(38, 38, 38, 0.5)' }}
className="last:border-0"
>
{/* Position */}
<Box
w="8"
h="8"
display="flex"
center
rounded="full"
style={{
fontSize: '0.75rem',
fontWeight: 'bold',
border: `1px solid ${medalBorder}`,
backgroundColor: medalBg,
color: medalColor
}}
>
{position <= 3 ? (
<Icon icon={Crown} size={3.5} />
) : (
position
)}
</Box>
{/* Team Info */}
<Box w="9" h="9" rounded="md" overflow="hidden" bg="bg-deep-graphite" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)' }} flexShrink={0}>
<Image
src={logoUrl}
alt={name}
width={36}
height={36}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</Box>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text weight="medium" color="text-white" className="truncate" block>
{name}
</Text>
<Stack direction="row" align="center" gap={2} wrap mt={0.5}>
{category && (
<Stack direction="row" align="center" gap={1}>
<Box w="1.5" h="1.5" rounded="full" bg="bg-purple-500" />
<Text size="xs" color="text-purple-400">{category}</Text>
</Stack>
)}
<Stack direction="row" align="center" gap={1}>
<Icon icon={Users} size={3} color="var(--text-gray-600)" />
<Text size="xs" color="text-gray-500">{memberCount}</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Trophy} size={3} color="var(--text-gray-600)" />
<Text size="xs" color="text-gray-500">{totalWins} wins</Text>
</Stack>
{isRecruiting && (
<Stack direction="row" align="center" gap={1}>
<Box w="1.5" h="1.5" rounded="full" bg="bg-performance-green" />
<Text size="xs" color="text-performance-green">Recruiting</Text>
</Stack>
)}
</Stack>
</Box>
{/* Rating */}
<Box textAlign="right">
<Text font="mono" weight="semibold" color="text-purple-400" block>
{typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'}
</Text>
<Text size="xs" color="text-gray-500">Rating</Text>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
import { TeamLogo } from './TeamLogo';
import { RankBadge } from '@/components/leaderboards/RankBadge';
interface TeamLeaderboardPanelProps {
teams: Array<{
id: string;
name: string;
logoUrl?: string;
rating: number;
wins: number;
races: number;
memberCount: number;
}>;
onTeamClick: (id: string) => void;
}
export function TeamLeaderboardPanel({ teams, onTeamClick }: TeamLeaderboardPanelProps) {
return (
<Box border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
<Table>
<TableHead className="bg-base-graphite/50">
<TableRow>
<TableHeader className="w-16 text-center">Rank</TableHeader>
<TableHeader>Team</TableHeader>
<TableHeader className="text-center">Rating</TableHeader>
<TableHeader className="text-center">Wins</TableHeader>
<TableHeader className="text-center">Races</TableHeader>
<TableHeader className="text-center">Members</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{teams.map((team, index) => (
<TableRow
key={team.id}
onClick={() => onTeamClick(team.id)}
clickable
className="group hover:bg-primary-blue/5 transition-colors border-b border-border-steel-grey/30 last:border-0"
>
<TableCell className="text-center">
<RankBadge rank={index + 1} />
</TableCell>
<TableCell>
<Stack direction="row" align="center" gap={3}>
<Box w="8" h="8" bg="base-graphite" border borderColor="border-steel-grey" display="flex" center overflow="hidden">
<TeamLogo teamId={team.id} alt={team.name} />
</Box>
<Text weight="bold" size="sm" color="text-white" className="group-hover:text-primary-blue transition-colors">
{team.name}
</Text>
</Stack>
</TableCell>
<TableCell className="text-center">
<Text font="mono" weight="bold" color="text-primary-blue">{team.rating}</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" color="text-gray-300">{team.wins}</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" color="text-gray-300">{team.races}</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" color="text-gray-400" size="xs">{team.memberCount}</Text>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
);
}

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Users } from 'lucide-react';
import { Icon } from '@/ui/Icon';
export interface TeamLogoProps {
teamId?: string;
src?: string;
alt: string;
size?: number;
className?: string;
border?: boolean;
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export function TeamLogo({
teamId,
src,
alt,
size = 48,
className = '',
border = true,
rounded = 'md',
}: TeamLogoProps) {
const logoSrc = src || (teamId ? `/media/teams/${teamId}/logo` : undefined);
return (
<Box
display="flex"
alignItems="center"
justifyContent="center"
rounded={rounded}
overflow="hidden"
bg="bg-charcoal-outline/10"
border={border}
borderColor="border-charcoal-outline/50"
className={className}
style={{ width: size, height: size, flexShrink: 0 }}
>
{logoSrc ? (
<Image
src={logoSrc}
alt={alt}
className="w-full h-full object-contain p-1"
fallbackSrc="/default-team-logo.png"
/>
) : (
<Icon icon={Users} size={size > 32 ? 5 : 4} color="text-gray-500" />
)}
</Box>
);
}

View File

@@ -1,26 +1,64 @@
import React from 'react';
import { TeamMembershipCard as UiTeamMembershipCard } from '@/ui/TeamMembershipCard';
import { routes } from '@/lib/routing/RouteConfig';
interface TeamMembership {
teamId: string;
teamName: string;
teamTag?: string;
role: string;
joinedAt: string;
}
import { ChevronRight, Users } from 'lucide-react';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
interface TeamMembershipCardProps {
membership: TeamMembership;
teamName: string;
role: string;
joinedAt: string;
href: string;
}
export function TeamMembershipCard({ membership }: TeamMembershipCardProps) {
export function TeamMembershipCard({
teamName,
role,
joinedAt,
href,
}: TeamMembershipCardProps) {
return (
<UiTeamMembershipCard
teamName={membership.teamName}
role={membership.role}
joinedAt={membership.joinedAt}
href={routes.team.detail(membership.teamId)}
/>
<Link href={href}>
<Surface
variant="muted"
padding={4}
rounded="xl"
border
style={{ borderColor: 'rgba(38, 38, 38, 0.8)' }}
className="flex items-center gap-4 hover:border-purple-400/30 hover:bg-iron-gray/50 transition-all group"
>
<Surface
variant="muted"
w="12"
h="12"
display="flex"
center
rounded="xl"
style={{ backgroundColor: 'rgba(147, 51, 234, 0.1)', borderColor: 'rgba(147, 51, 234, 0.2)' }}
border
>
<Icon icon={Users} size={6} color="var(--neon-purple)" />
</Surface>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text weight="semibold" color="text-white" className="truncate group-hover:text-purple-400 transition-colors" block>
{teamName}
</Text>
<Stack direction="row" align="center" gap={2} mt={1}>
<Badge variant="primary" style={{ backgroundColor: 'rgba(147, 51, 234, 0.1)', color: 'var(--neon-purple)', textTransform: 'capitalize' }}>
{role}
</Badge>
<Text size="xs" color="text-gray-400">
Since {new Date(joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Text>
</Stack>
</Box>
<Icon icon={ChevronRight} size={4} color="var(--text-gray-500)" className="group-hover:text-purple-400 transition-colors" />
</Surface>
</Link>
);
}

View File

@@ -0,0 +1,62 @@
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { ChevronRight, Shield, Users } from 'lucide-react';
interface TeamMembership {
team: {
id: string;
name: string;
};
role: string;
joinedAt: Date;
}
interface TeamMembershipGridProps {
memberships: TeamMembership[];
}
export function TeamMembershipGrid({ memberships }: TeamMembershipGridProps) {
return (
<Card>
<Box mb={4}>
<Heading level={2} icon={<Shield style={{ width: '1.25rem', height: '1.25rem', color: '#a855f7' }} />}>
Team Memberships
<Text size="sm" color="text-gray-500" weight="normal" style={{ marginLeft: '0.5rem' }}>({memberships.length})</Text>
</Heading>
</Box>
<Box style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: '1rem' }}>
{memberships.map((membership) => (
<Box key={membership.team.id}>
<Link
href={`/teams/${membership.team.id}`}
variant="ghost"
>
<Surface variant="muted" rounded="xl" border padding={4} style={{ display: 'flex', alignItems: 'center', gap: '1rem', backgroundColor: 'rgba(38, 38, 38, 0.3)', borderColor: '#262626' }}>
<Surface variant="muted" rounded="lg" padding={3} style={{ backgroundColor: 'rgba(147, 51, 234, 0.2)', border: '1px solid rgba(147, 51, 234, 0.3)' }}>
<Users style={{ width: '1.5rem', height: '1.5rem', color: '#a855f7' }} />
</Surface>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text weight="semibold" color="text-white" block truncate>{membership.team.name}</Text>
<Stack direction="row" align="center" gap={2} mt={1}>
<Surface variant="muted" rounded="full" padding={1} style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem', backgroundColor: 'rgba(147, 51, 234, 0.2)', color: '#a855f7' }}>
<Text size="xs" weight="medium" style={{ textTransform: 'capitalize' }}>{membership.role}</Text>
</Surface>
<Text size="xs" color="text-gray-500">Since {membership.joinedAt.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}</Text>
</Stack>
</Box>
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373' }} />
</Surface>
</Link>
</Box>
))}
</Box>
</Card>
);
}

View File

@@ -6,7 +6,7 @@ import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
import { RankBadge } from '@/ui/RankBadge';
import { RankBadge } from '@/components/leaderboards/RankBadge';
import { TeamIdentity } from '@/components/teams/TeamIdentity';
import { getMediaUrl } from '@/lib/utilities/media';

View File

@@ -11,7 +11,7 @@ import { Heading } from '@/ui/Heading';
import { Select } from '@/ui/Select';
import { Button } from '@/ui/Button';
import { routes } from '@/lib/routing/RouteConfig';
import { TeamRosterList } from '@/ui/TeamRosterList';
import { TeamRosterList } from '@/components/teams/TeamRosterList';
import { TeamRosterItem } from '@/components/teams/TeamRosterItem';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
import { sortMembers } from '@/lib/utilities/roster-utils';

View File

@@ -0,0 +1,14 @@
import React, { ReactNode } from 'react';
import { Stack } from '@/ui/Stack';
interface TeamRosterListProps {
children: ReactNode;
}
export function TeamRosterList({ children }: TeamRosterListProps) {
return (
<Stack gap={3}>
{children}
</Stack>
);
}

View File

@@ -0,0 +1,30 @@
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/Stack';
import { Search } from 'lucide-react';
interface TeamSearchBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
}
export function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) {
return (
<Box id="teams-list" mb={6}>
<Stack direction="row" gap={4} wrap>
<Box flexGrow={1}>
<Input
type="text"
placeholder="Search teams by name, description, region, or language..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
icon={<Icon icon={Search} size={5} color="var(--text-gray-500)" />}
/>
</Box>
</Stack>
</Box>
);
}

View File

@@ -4,7 +4,7 @@ import { Card } from '@/ui/Card';
import { useTeamStandings } from "@/hooks/team/useTeamStandings";
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { StandingsList } from '@/ui/StandingsList';
import { StandingsList } from '@/components/races/StandingsList';
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
import { EmptyState } from '@/components/shared/state/EmptyState';
import { Trophy } from 'lucide-react';

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface TeamStatItemProps {
label: string;
value: string | number;
color?: string;
align?: 'left' | 'center' | 'right';
}
export function TeamStatItem({ label, value, color = 'text-white', align = 'left' }: TeamStatItemProps) {
return (
<Box
p={2}
rounded="lg"
bg="bg-iron-gray/30"
display="flex"
flexDirection="col"
alignItems={align === 'center' ? 'center' : align === 'right' ? 'end' : 'start'}
>
<Text size="xs" color="text-gray-500" block mb={0.5}>{label}</Text>
<Text size="sm" weight="semibold" color={color}>{value}</Text>
</Box>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface TeamTagProps {
tag: string;
}
export function TeamTag({ tag }: TeamTagProps) {
return (
<Box
bg="bg-deep-graphite"
rounded="full"
px={2}
py={0.5}
display="inline-block"
>
<Text size="xs" color="text-gray-300">[{tag}]</Text>
</Box>
);
}