website refactor

This commit is contained in:
2026-01-15 19:55:46 +01:00
parent 5ef149b782
commit ce7be39155
154 changed files with 436 additions and 356 deletions

View File

@@ -12,7 +12,7 @@ import { Heading } from '@/ui/Heading';
import { JoinRequestList } from '@/ui/JoinRequestList';
import { JoinRequestItem } from '@/ui/JoinRequestItem';
import { DangerZone } from '@/ui/DangerZone';
import { MinimalEmptyState } from '@/ui/EmptyState';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
import { useTeamJoinRequests, useUpdateTeam, useApproveJoinRequest, useRejectJoinRequest } from "@/hooks/team";
import type { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';

View File

@@ -0,0 +1,79 @@
import { JoinTeamButton } from '@/components/teams/JoinTeamButton';
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 { Text } from '@/ui/Text';
interface TeamHeroProps {
team: {
id: string;
name: string;
tag: string | null;
description?: string;
category?: string | null;
createdAt?: string;
leagues: { id: string }[];
};
memberCount: number;
onUpdate: () => void;
}
export function TeamHero({ team, memberCount, onUpdate }: TeamHeroProps) {
return (
<Card>
<Stack direction="row" align="start" justify="between" wrap gap={6}>
<Stack direction="row" align="start" gap={6} wrap flexGrow={1}>
<Box
w="24"
h="24"
rounded="lg"
p={1}
overflow="hidden"
bg="bg-deep-graphite"
display="flex"
alignItems="center"
justifyContent="center"
>
<TeamLogo teamId={team.id} alt={team.name} />
</Box>
<Box flexGrow={1} minWidth="0">
<Stack direction="row" align="center" gap={3} mb={2}>
<Heading level={1}>{team.name}</Heading>
{team.tag && <TeamTag tag={team.tag} />}
</Stack>
<Text color="text-gray-300" block mb={4} maxWidth="42rem">{team.description}</Text>
<Stack direction="row" align="center" gap={4} wrap>
<Text size="sm" color="text-gray-400">{memberCount} {memberCount === 1 ? 'member' : 'members'}</Text>
{team.category && (
<Stack direction="row" align="center" gap={1.5}>
<Box w="2" h="2" rounded="full" bg="bg-purple-500" />
<Text size="sm" color="text-purple-400">{team.category}</Text>
</Stack>
)}
{team.createdAt && (
<Text size="sm" color="text-gray-400">
Founded {new Date(team.createdAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Text>
)}
{team.leagues && team.leagues.length > 0 && (
<Text size="sm" color="text-gray-400">
Active in {team.leagues.length} {team.leagues.length === 1 ? 'league' : 'leagues'}
</Text>
)}
</Stack>
</Box>
</Stack>
<JoinTeamButton teamId={team.id} onUpdate={onUpdate} />
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,83 @@
import { Users } from 'lucide-react';
import { ReactNode } from 'react';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface TeamHeroSectionProps {
title: ReactNode;
description: string;
statsContent: ReactNode;
actionsContent: ReactNode;
sideContent: ReactNode;
}
export function TeamHeroSection({
title,
description,
statsContent,
actionsContent,
sideContent,
}: TeamHeroSectionProps) {
return (
<Box position="relative" mb={10} overflow="hidden">
{/* Main Hero Card */}
<Box
position="relative"
py={12}
px={8}
rounded="2xl"
style={{
background: 'linear-gradient(to bottom right, rgba(147, 51, 234, 0.3), rgba(38, 38, 38, 0.8), #0f1115)',
borderColor: 'rgba(147, 51, 234, 0.2)',
}}
border
>
{/* Background decorations */}
<Box position="absolute" top="0" right="0" w="80" h="80" bg="bg-purple-500" bgOpacity={0.1} rounded="full" blur="3xl" />
<Box position="absolute" bottom="0" left="1/4" w="64" h="64" bg="bg-neon-aqua" bgOpacity={0.05} rounded="full" blur="3xl" />
<Box position="absolute" top="1/2" right="1/4" w="48" h="48" bg="bg-yellow-400" bgOpacity={0.05} rounded="full" blur="2xl" />
<Box position="relative" zIndex={10}>
<Stack direction={{ base: 'col', lg: 'row' }} align="start" justify="between" gap={8}>
<Box maxWidth="xl">
{/* Badge */}
<Box mb={4}>
<Badge variant="primary" icon={Users}>
Team Racing
</Badge>
</Box>
<Heading level={1}>
{title}
</Heading>
<Text size="lg" color="text-gray-400" leading="relaxed" block mt={4} mb={6}>
{description}
</Text>
{/* Quick Stats */}
<Stack direction="row" gap={4} mb={6} wrap>
{statsContent}
</Stack>
{/* CTA Buttons */}
<Stack direction="row" gap={3} wrap>
{actionsContent}
</Stack>
</Box>
{/* Side Content */}
<Box w={{ base: 'full', lg: '72' }}>
{sideContent}
</Box>
</Stack>
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,147 @@
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { SkillLevelButton } from '@/ui/SkillLevelButton';
import { Stack } from '@/ui/Stack';
import { TeamHeroSection as UiTeamHeroSection } from '@/components/teams/TeamHeroSection';
import { TeamHeroStats } from '@/components/teams/TeamHeroStats';
import { Text } from '@/ui/Text';
import {
Crown,
LucideIcon,
Plus,
Search,
Shield,
Star,
TrendingUp,
} from 'lucide-react';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
interface SkillLevelConfig {
id: SkillLevel;
label: string;
icon: LucideIcon;
color: string;
bgColor: string;
borderColor: string;
description: string;
}
const SKILL_LEVELS: SkillLevelConfig[] = [
{
id: 'pro',
label: 'Pro',
icon: Crown,
color: 'text-warning-amber',
bgColor: 'bg-yellow-400/10',
borderColor: 'border-yellow-400/30',
description: 'Elite competition, sponsored teams',
},
{
id: 'advanced',
label: 'Advanced',
icon: Star,
color: 'text-purple-400',
bgColor: 'bg-purple-900/10',
borderColor: 'border-purple-900/30',
description: 'Competitive racing, high consistency',
},
{
id: 'intermediate',
label: 'Intermediate',
icon: TrendingUp,
color: 'text-primary-blue',
bgColor: 'bg-blue-900/10',
borderColor: 'border-blue-900/30',
description: 'Growing skills, regular practice',
},
{
id: 'beginner',
label: 'Beginner',
icon: Shield,
color: 'text-performance-green',
bgColor: 'bg-green-900/10',
borderColor: 'border-green-900/30',
description: 'Learning the basics, friendly environment',
},
];
interface TeamHeroSectionProps {
teams: TeamSummaryViewModel[];
teamsByLevel: Record<string, TeamSummaryViewModel[]>;
recruitingCount: number;
onShowCreateForm: () => void;
onBrowseTeams: () => void;
onSkillLevelClick: (level: SkillLevel) => void;
}
export function TeamHeroSection({
teams,
teamsByLevel,
recruitingCount,
onShowCreateForm,
onBrowseTeams,
onSkillLevelClick,
}: TeamHeroSectionProps) {
return (
<UiTeamHeroSection
title={
<>
Find Your
<Text color="text-purple-400"> Crew</Text>
</>
}
description="Solo racing is great. Team racing is unforgettable. Join a team that matches your skill level and ambitions."
statsContent={
<TeamHeroStats teamCount={teams.length} recruitingCount={recruitingCount} />
}
actionsContent={
<>
<Button
variant="primary"
onClick={onShowCreateForm}
icon={<Icon icon={Plus} size={4} />}
bg="bg-purple-600"
>
Create Team
</Button>
<Button
variant="secondary"
onClick={onBrowseTeams}
icon={<Icon icon={Search} size={4} />}
>
Browse Teams
</Button>
</>
}
sideContent={
<>
<Text size="xs" color="text-gray-500" weight="medium" block mb={3} uppercase letterSpacing="0.05em">
Find Your Level
</Text>
<Stack gap={2}>
{SKILL_LEVELS.map((level) => {
const count = teamsByLevel[level.id]?.length || 0;
return (
<SkillLevelButton
key={level.id}
label={level.label}
icon={level.icon}
color={level.color}
bgColor={level.bgColor}
borderColor={level.borderColor}
count={count}
onClick={() => onSkillLevelClick(level.id)}
/>
);
})}
</Stack>
</>
}
/>
);
}

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Users, UserPlus } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
interface TeamHeroStatsProps {
teamCount: number;
recruitingCount: number;
}
export function TeamHeroStats({ teamCount, recruitingCount }: TeamHeroStatsProps) {
return (
<Stack direction="row" align="center" gap={4}>
<Box
display="flex"
alignItems="center"
gap={2}
px={3}
py={2}
rounded="lg"
bg="bg-iron-gray/50"
border={true}
borderColor="border-charcoal-outline"
>
<Icon icon={Users} size={4} color="text-purple-400" />
<Text weight="semibold" color="text-white">{teamCount}</Text>
<Text size="sm" color="text-gray-500">Teams</Text>
</Box>
<Box
display="flex"
alignItems="center"
gap={2}
px={3}
py={2}
rounded="lg"
bg="bg-iron-gray/50"
border={true}
borderColor="border-charcoal-outline"
>
<Icon icon={UserPlus} size={4} color="text-performance-green" />
<Text weight="semibold" color="text-white">{recruitingCount}</Text>
<Text size="sm" color="text-gray-500">Recruiting</Text>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Image } from '@/ui/Image';
interface TeamIdentityProps {
name: string;
logoUrl: string;
performanceLevel?: string;
category?: string;
}
export function TeamIdentity({ name, logoUrl, performanceLevel, category }: TeamIdentityProps) {
return (
<Stack direction="row" align="center" gap={3}>
<Box width="10" height="10" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline">
<Image
src={logoUrl}
alt={name}
width={40}
height={40}
fullWidth
fullHeight
objectFit="cover"
/>
</Box>
<Box flex={1}>
<Text weight="semibold" color="text-white" block truncate>{name}</Text>
{(performanceLevel || category) && (
<Stack direction="row" align="center" gap={2} mt={1} wrap>
{performanceLevel && (
<Text size="xs" color="text-gray-500">{performanceLevel}</Text>
)}
{category && (
<Stack direction="row" align="center" gap={1}>
<Box width="1.5" height="1.5" rounded="full" bg="bg-primary-blue" opacity={0.5} />
<Text size="xs" color="text-primary-blue">{category}</Text>
</Stack>
)}
</Stack>
)}
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { getMediaUrl } from '@/lib/utilities/media';
import { TeamLeaderboardItem } from '@/ui/TeamLeaderboardItem';
import { TeamLeaderboardPreview as UiTeamLeaderboardPreview } from '@/ui/TeamLeaderboardPreview';
interface TeamLeaderboardPreviewProps {
topTeams: Array<{
id: string;
name: string;
logoUrl?: string;
category?: string;
memberCount: number;
totalWins: number;
isRecruiting: boolean;
rating?: number;
performanceLevel: string;
}>;
onTeamClick: (id: string) => void;
onViewFullLeaderboard: () => void;
}
export function TeamLeaderboardPreview({
topTeams,
onTeamClick,
onViewFullLeaderboard
}: TeamLeaderboardPreviewProps) {
const getMedalColor = (position: number) => {
switch (position) {
case 0: return '#facc15';
case 1: return '#d1d5db';
case 2: return '#d97706';
default: return '#6b7280';
}
};
const getMedalBg = (position: number) => {
switch (position) {
case 0: return 'rgba(250, 204, 21, 0.1)';
case 1: return 'rgba(209, 213, 219, 0.1)';
case 2: return 'rgba(217, 119, 6, 0.1)';
default: return 'rgba(38, 38, 38, 0.5)';
}
};
const getMedalBorder = (position: number) => {
switch (position) {
case 0: return 'rgba(250, 204, 21, 0.3)';
case 1: return 'rgba(209, 213, 219, 0.3)';
case 2: return 'rgba(217, 119, 6, 0.3)';
default: return 'rgba(38, 38, 38, 1)';
}
};
if (topTeams.length === 0) return null;
return (
<UiTeamLeaderboardPreview
title="Top Teams"
subtitle="Highest rated racing teams"
onViewFull={onViewFullLeaderboard}
>
{topTeams.map((team, index) => (
<TeamLeaderboardItem
key={team.id}
position={index + 1}
name={team.name}
logoUrl={team.logoUrl || getMediaUrl('team-logo', team.id)}
category={team.category}
memberCount={team.memberCount}
totalWins={team.totalWins}
isRecruiting={team.isRecruiting}
rating={team.rating}
onClick={() => onTeamClick(team.id)}
medalColor={getMedalColor(index)}
medalBg={getMedalBg(index)}
medalBorder={getMedalBorder(index)}
/>
))}
</UiTeamLeaderboardPreview>
);
}

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { Trophy, Crown, Users } from 'lucide-react';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { getMediaUrl } from '@/lib/utilities/media';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Button } from '@/ui/Button';
import { Image } from '@/ui/Image';
import { Podium, PodiumItem } from '@/ui/Podium';
interface TeamPodiumProps {
teams: TeamSummaryViewModel[];
onClick: (id: string) => void;
}
export function TeamPodium({ teams, onClick }: TeamPodiumProps) {
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
if (teams.length < 3) return null;
// Display order: 2nd, 1st, 3rd
const podiumOrder: [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel] = [
top3[1],
top3[0],
top3[2],
];
const podiumHeights = ['28', '36', '20'];
const podiumPositions = [2, 1, 3];
const getPositionColor = (position: number) => {
switch (position) {
case 1:
return 'text-yellow-400';
case 2:
return 'text-gray-300';
case 3:
return 'text-amber-600';
default:
return 'text-gray-500';
}
};
const getBgColor = (position: number) => {
switch (position) {
case 1:
return 'bg-gradient-to-br from-primary-blue/20 via-iron-gray/80 to-deep-graphite';
case 2:
return 'bg-iron-gray';
case 3:
return 'bg-gradient-to-br from-purple-600/20 via-iron-gray/80 to-deep-graphite';
default:
return 'bg-iron-gray/50';
}
};
return (
<Podium title="Top 3 Teams">
{podiumOrder.map((team, index) => {
const position = podiumPositions[index] ?? 0;
return (
<PodiumItem
key={team.id}
position={position}
height={podiumHeights[index] || '20'}
bgColor={getBgColor(position)}
positionColor={getPositionColor(position)}
cardContent={
<Button
variant="ghost"
onClick={() => onClick(team.id)}
h="auto"
mb={4}
p={0}
className="transition-all"
>
<Box
bg={getBgColor(position)}
rounded="xl"
border={true}
borderColor="border-charcoal-outline"
p={4}
position="relative"
>
{/* Crown for 1st place */}
{position === 1 && (
<Box position="absolute" top="-4" left="1/2" translateX="-1/2">
<Box position="relative">
<Box animate="pulse">
<Icon icon={Crown} size={8} color="text-warning-amber" />
</Box>
<Box position="absolute" inset="0" bg="bg-yellow-400" bgOpacity={0.3} blur="md" rounded="full" />
</Box>
</Box>
)}
{/* Team logo */}
<Box h="20" w="20" display="flex" center rounded="xl" bg="bg-deep-graphite" border={true} borderColor="border-charcoal-outline" overflow="hidden" mb={3}>
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={80}
height={80}
objectFit="cover"
/>
</Box>
{/* Team name */}
<Text weight="bold" size="sm" color="text-white" align="center" block truncate maxWidth="28">
{team.name}
</Text>
{/* Category */}
{team.category && (
<Text size="xs" color="text-primary-blue" align="center" block mt={1}>
{team.category}
</Text>
)}
{/* Rating placeholder */}
<Text size="xl" weight="bold" color={getPositionColor(position)} align="center" block mt={1}>
</Text>
{/* Stats row */}
<Stack direction="row" align="center" justify="center" gap={3} mt={2}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Trophy} size={3} color="text-performance-green" />
<Text size="xs" color="text-gray-400">{team.totalWins}</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Users} size={3} color="text-primary-blue" />
<Text size="xs" color="text-gray-400">{team.memberCount}</Text>
</Stack>
</Stack>
</Box>
</Button>
}
/>
);
})}
</Podium>
);
}

View File

@@ -0,0 +1,104 @@
import React from 'react';
import { Users } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
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 { TeamIdentity } from '@/components/teams/TeamIdentity';
import { getMediaUrl } from '@/lib/utilities/media';
interface Team {
id: string;
name: string;
logoUrl?: string;
performanceLevel: string;
category?: string;
region?: string;
languages?: string[];
isRecruiting?: boolean;
memberCount: number;
totalWins: number;
totalRaces: number;
}
interface TeamRankingsTableProps {
teams: Team[];
sortBy: string;
onTeamClick: (id: string) => void;
}
export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTableProps) {
return (
<Card p={0} overflow="hidden">
<Table>
<TableHead>
<TableRow>
<TableHeader>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Rank</Text>
</TableHeader>
<TableHeader>
<Text size="xs" weight="medium" color="text-gray-500" block>Team</Text>
</TableHeader>
<TableHeader>
<Box display={{ base: 'none', lg: 'block' }}>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Members</Text>
</Box>
</TableHeader>
<TableHeader>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Rating</Text>
</TableHeader>
<TableHeader>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Wins</Text>
</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{teams.map((team, index) => (
<TableRow
key={team.id}
onClick={() => onTeamClick(team.id)}
clickable
>
<TableCell>
<RankBadge rank={index + 1} />
</TableCell>
<TableCell>
<TeamIdentity
name={team.name}
logoUrl={team.logoUrl || getMediaUrl('team-logo', team.id)}
performanceLevel={team.performanceLevel}
category={team.category}
/>
</TableCell>
<TableCell>
<Box display={{ base: 'none', lg: 'flex' }} alignItems="center" justifyContent="center">
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Users} size={3.5} color="text-gray-500" />
<Text size="sm" color="text-gray-400">{team.memberCount}</Text>
</Stack>
</Box>
</TableCell>
<TableCell>
<Box display="flex" center>
<Text font="mono" weight="semibold" color={sortBy === 'rating' ? 'text-primary-blue' : 'text-white'}>
0
</Text>
</Box>
</TableCell>
<TableCell>
<Box display="flex" center>
<Text font="mono" weight="semibold" color={sortBy === 'wins' ? 'text-primary-blue' : 'text-white'}>
{team.totalWins}
</Text>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
);
}

View File

@@ -12,8 +12,8 @@ import { Select } from '@/ui/Select';
import { Button } from '@/ui/Button';
import { routes } from '@/lib/routing/RouteConfig';
import { TeamRosterList } from '@/ui/TeamRosterList';
import { TeamRosterItem } from '@/ui/TeamRosterItem';
import { MinimalEmptyState } from '@/ui/EmptyState';
import { TeamRosterItem } from '@/components/teams/TeamRosterItem';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
import { sortMembers } from '@/lib/utilities/roster-utils';
export type TeamRole = 'owner' | 'admin' | 'member';

View File

@@ -0,0 +1,73 @@
import React, { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { DriverIdentity } from '@/components/drivers/DriverIdentity';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
interface TeamRosterItemProps {
driver: DriverViewModel;
href: string;
roleLabel: string;
joinedAt: string | Date;
rating: number | null;
overallRank: number | null;
actions?: ReactNode;
}
export function TeamRosterItem({
driver,
href,
roleLabel,
joinedAt,
rating,
overallRank,
actions,
}: TeamRosterItemProps) {
return (
<Box
bg="bg-iron-gray/50"
rounded="lg"
border={true}
borderColor="border-charcoal-outline"
p={4}
>
<Stack direction="row" align="center" justify="between" wrap gap={4}>
<DriverIdentity
driver={driver}
href={href}
contextLabel={roleLabel}
meta={
<Text size="xs" color="text-gray-400">
{driver.country} Joined {new Date(joinedAt).toLocaleDateString()}
</Text>
}
size="md"
/>
{rating !== null && (
<Stack direction="row" align="center" gap={6}>
<Box display="flex" flexDirection="col" alignItems="center">
<Text size="lg" weight="bold" color="text-primary-blue" block>
{rating}
</Text>
<Text size="xs" color="text-gray-400">Rating</Text>
</Box>
{overallRank !== null && (
<Box display="flex" flexDirection="col" alignItems="center">
<Text size="sm" color="text-gray-300" block>#{overallRank}</Text>
<Text size="xs" color="text-gray-500">Rank</Text>
</Box>
)}
</Stack>
)}
{actions && (
<Stack direction="row" align="center" gap={2}>
{actions}
</Stack>
)}
</Stack>
</Box>
);
}

View File

@@ -5,8 +5,8 @@ import { useTeamStandings } from "@/hooks/team/useTeamStandings";
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { StandingsList } from '@/ui/StandingsList';
import { LoadingWrapper } from '@/ui/LoadingWrapper';
import { EmptyState } from '@/ui/EmptyState';
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
import { EmptyState } from '@/components/shared/state/EmptyState';
import { Trophy } from 'lucide-react';
interface TeamStandingsProps {

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { Trophy, Crown, Users } from 'lucide-react';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { getMediaUrl } from '@/lib/utilities/media';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Button } from '@/ui/Button';
import { Image } from '@/ui/Image';
import { Podium, PodiumItem } from '@/ui/Podium';
interface TopThreePodiumProps {
teams: TeamSummaryViewModel[];
onClick: (id: string) => void;
}
export function TopThreePodium({ teams, onClick }: TopThreePodiumProps) {
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
if (teams.length < 3) return null;
// Display order: 2nd, 1st, 3rd
const podiumOrder: [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel] = [
top3[1],
top3[0],
top3[2],
];
const podiumHeights = ['28', '36', '20'];
const podiumPositions = [2, 1, 3];
const getPositionColor = (position: number) => {
switch (position) {
case 1:
return 'text-yellow-400';
case 2:
return 'text-gray-300';
case 3:
return 'text-amber-600';
default:
return 'text-gray-500';
}
};
const getBgColor = (position: number) => {
switch (position) {
case 1:
return 'bg-gradient-to-br from-primary-blue/20 via-iron-gray/80 to-deep-graphite';
case 2:
return 'bg-iron-gray';
case 3:
return 'bg-gradient-to-br from-purple-600/20 via-iron-gray/80 to-deep-graphite';
default:
return 'bg-iron-gray/50';
}
};
return (
<Podium title="Top 3 Teams">
{podiumOrder.map((team, index) => {
const position = podiumPositions[index] ?? 0;
return (
<PodiumItem
key={team.id}
position={position}
height={podiumHeights[index] || '20'}
bgColor={getBgColor(position)}
positionColor={getPositionColor(position)}
cardContent={
<Button
variant="ghost"
onClick={() => onClick(team.id)}
h="auto"
mb={4}
p={0}
transition
>
<Box
bg={getBgColor(position)}
rounded="xl"
border
borderColor="border-charcoal-outline"
p={4}
position="relative"
>
{/* Crown for 1st place */}
{position === 1 && (
<Box position="absolute" top="-4" left="1/2" translateX="-1/2">
<Box position="relative">
<Box animate="pulse">
<Icon icon={Crown} size={8} color="var(--warning-amber)" />
</Box>
<Box position="absolute" inset="0" bg="bg-yellow-400" bgOpacity={0.3} blur="md" rounded="full" />
</Box>
</Box>
)}
{/* Team logo */}
<Box h="20" w="20" display="flex" center rounded="xl" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" overflow="hidden" mb={3}>
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={80}
height={80}
objectFit="cover"
/>
</Box>
{/* Team name */}
<Text weight="bold" size="sm" color="text-white" textAlign="center" block truncate maxWidth="28">
{team.name}
</Text>
{/* Category */}
{team.category && (
<Text size="xs" color="text-primary-blue" textAlign="center" block mt={1}>
{team.category}
</Text>
)}
{/* Rating placeholder */}
<Text size="xl" weight="bold" color={getPositionColor(position)} textAlign="center" block mt={1}>
</Text>
{/* Stats row */}
<Stack direction="row" align="center" justify="center" gap={3} mt={2}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Trophy} size={3} color="var(--performance-green)" />
<Text size="xs" color="text-gray-400">{team.totalWins}</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Users} size={3} color="var(--primary-blue)" />
<Text size="xs" color="text-gray-400">{team.memberCount}</Text>
</Stack>
</Stack>
</Box>
</Button>
}
/>
);
})}
</Podium>
);
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import {
Handshake,
MessageCircle,
Calendar,
Trophy,
LucideIcon,
} from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { BenefitCard } from '@/components/landing/BenefitCard';
interface Benefit {
icon: LucideIcon;
title: string;
description: string;
}
export function WhyJoinTeamSection() {
const benefits: Benefit[] = [
{
icon: Handshake,
title: 'Shared Strategy',
description: 'Develop setups together, share telemetry, and coordinate pit strategies for endurance races.',
},
{
icon: MessageCircle,
title: 'Team Communication',
description: 'Discord integration, voice chat during races, and dedicated team channels.',
},
{
icon: Calendar,
title: 'Coordinated Schedule',
description: 'Team calendars, practice sessions, and organized race attendance.',
},
{
icon: Trophy,
title: 'Team Championships',
description: 'Compete in team-based leagues and build your collective reputation.',
},
];
return (
<Box mb={12}>
<Box textAlign="center" mb={8}>
<Box mb={2}>
<Heading level={2}>Why Join a Team?</Heading>
</Box>
<Text color="text-gray-400">Racing is better when you have teammates to share the journey</Text>
</Box>
<Grid cols={4} gap={4}>
{benefits.map((benefit) => (
<BenefitCard
key={benefit.title}
icon={benefit.icon}
title={benefit.title}
description={benefit.description}
/>
))}
</Grid>
</Box>
);
}