website refactor
This commit is contained in:
@@ -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<{
|
||||
|
||||
80
apps/website/components/teams/RecruitingTeamCard.tsx
Normal file
80
apps/website/components/teams/RecruitingTeamCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
apps/website/components/teams/RecruitingTeamGrid.tsx
Normal file
14
apps/website/components/teams/RecruitingTeamGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
137
apps/website/components/teams/TeamCardWrapper.tsx
Normal file
137
apps/website/components/teams/TeamCardWrapper.tsx
Normal 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" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
116
apps/website/components/teams/TeamFilter.tsx
Normal file
116
apps/website/components/teams/TeamFilter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
117
apps/website/components/teams/TeamHeaderPanel.tsx
Normal file
117
apps/website/components/teams/TeamHeaderPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
73
apps/website/components/teams/TeamLadderRow.tsx
Normal file
73
apps/website/components/teams/TeamLadderRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
26
apps/website/components/teams/TeamLadderTable.tsx
Normal file
26
apps/website/components/teams/TeamLadderTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
127
apps/website/components/teams/TeamLeaderboardItem.tsx
Normal file
127
apps/website/components/teams/TeamLeaderboardItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
75
apps/website/components/teams/TeamLeaderboardPanel.tsx
Normal file
75
apps/website/components/teams/TeamLeaderboardPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
apps/website/components/teams/TeamLogo.tsx
Normal file
53
apps/website/components/teams/TeamLogo.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
62
apps/website/components/teams/TeamMembershipGrid.tsx
Normal file
62
apps/website/components/teams/TeamMembershipGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
14
apps/website/components/teams/TeamRosterList.tsx
Normal file
14
apps/website/components/teams/TeamRosterList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
apps/website/components/teams/TeamSearchBar.tsx
Normal file
30
apps/website/components/teams/TeamSearchBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
26
apps/website/components/teams/TeamStatItem.tsx
Normal file
26
apps/website/components/teams/TeamStatItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
21
apps/website/components/teams/TeamTag.tsx
Normal file
21
apps/website/components/teams/TeamTag.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user