website refactor

This commit is contained in:
2026-01-18 22:55:55 +01:00
parent b43a23a48c
commit aeaa43f4d3
179 changed files with 4736 additions and 6832 deletions

View File

@@ -2,21 +2,22 @@
import { JoinRequestItem } from '@/components/leagues/JoinRequestItem';
import { JoinRequestList } from '@/components/leagues/JoinRequestList';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
import { EmptyState } from '@/ui/EmptyState';
import { LoadingWrapper } from '@/ui/LoadingWrapper';
import { useApproveJoinRequest } from "@/hooks/team/useApproveJoinRequest";
import { useRejectJoinRequest } from "@/hooks/team/useRejectJoinRequest";
import { useTeamJoinRequests } from "@/hooks/team/useTeamJoinRequests";
import { useUpdateTeam } from "@/hooks/team/useUpdateTeam";
import type { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Panel } from '@/ui/Panel';
import { DangerZone } from '@/ui/DangerZone';
import { Heading } from '@/ui/Heading';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
import { SectionHeader } from '@/ui/SectionHeader';
import { useState } from 'react';
import React from 'react';
interface TeamAdminProps {
team: {
@@ -74,14 +75,10 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
});
const handleApprove = () => {
// Note: The current API doesn't support approving specific requests
// This would need the requestId to be passed to the service
approveJoinRequestMutation.mutate();
};
const handleReject = () => {
// Note: The current API doesn't support rejecting specific requests
// This would need the requestId to be passed to the service
rejectJoinRequestMutation.mutate();
};
@@ -97,42 +94,30 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
};
return (
<Stack gap={6}>
<Card>
<Stack display="flex" alignItems="center" justifyContent="between" mb={6}>
<Heading level={3}>Team Settings</Heading>
{!editMode && (
<Button variant="secondary" onClick={() => setEditMode(true)}>
Edit Details
</Button>
)}
</Stack>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Panel
title="Team Settings"
actions={!editMode && (
<Button variant="secondary" size="sm" onClick={() => setEditMode(true)}>
Edit Details
</Button>
)}
>
{editMode ? (
<Stack gap={4}>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Team Name
</Text>
<Input
type="text"
value={editedTeam.name}
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
/>
</Stack>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Input
label="Team Name"
value={editedTeam.name}
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
/>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Team Tag
</Text>
<Input
type="text"
value={editedTeam.tag}
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
maxLength={4}
/>
<Text size="xs" color="text-gray-500" block mt={1}>Max 4 characters</Text>
</Stack>
<Input
label="Team Tag"
value={editedTeam.tag}
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
maxLength={4}
hint="Max 4 characters"
/>
<TextArea
label="Description"
@@ -141,7 +126,7 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
/>
<Stack direction="row" gap={2}>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<Button variant="primary" onClick={handleSaveChanges} disabled={updateTeamMutation.isPending}>
{updateTeamMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
@@ -158,33 +143,29 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
>
Cancel
</Button>
</Stack>
</Stack>
</div>
</div>
) : (
<Stack gap={4}>
<Stack>
<Text size="sm" color="text-gray-400" block>Team Name</Text>
<Text color="text-white" weight="medium" block>{team.name}</Text>
</Stack>
<Stack>
<Text size="sm" color="text-gray-400" block>Team Tag</Text>
<Text color="text-white" weight="medium" block>{team.tag}</Text>
</Stack>
<Stack>
<Text size="sm" color="text-gray-400" block>Description</Text>
<Text color="text-white" block>{team.description}</Text>
</Stack>
</Stack>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<Text size="sm" variant="low" block marginBottom={1}>Team Name</Text>
<Text variant="high" weight="medium" block>{team.name}</Text>
</div>
<div>
<Text size="sm" variant="low" block marginBottom={1}>Team Tag</Text>
<Text variant="high" weight="medium" block>{team.tag}</Text>
</div>
<div>
<Text size="sm" variant="low" block marginBottom={1}>Description</Text>
<Text variant="high" block>{team.description}</Text>
</div>
</div>
)}
</Card>
<Card>
<Heading level={3} mb={6}>Join Requests</Heading>
</Panel>
<Panel title="Join Requests">
{loading ? (
<Stack textAlign="center" py={8}>
<Text color="text-gray-400">Loading requests...</Text>
</Stack>
<LoadingWrapper variant="spinner" message="Loading requests..." />
) : joinRequests.length > 0 ? (
<JoinRequestList>
{joinRequests.map((request: TeamJoinRequestViewModel) => (
@@ -200,12 +181,13 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
))}
</JoinRequestList>
) : (
<MinimalEmptyState
<EmptyState
title="No pending join requests"
description="When drivers request to join your team, they will appear here."
variant="minimal"
/>
)}
</Card>
</Panel>
<DangerZone
title="Disband Team"
@@ -215,6 +197,6 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
Disband Team (Coming Soon)
</Button>
</DangerZone>
</Stack>
</div>
);
}

View File

@@ -1,19 +1,9 @@
'use client';
import { Badge } from '@/ui/Badge';
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/primitives/Stack';
import { Text } from '@/ui/Text';
import {
ChevronRight,
Globe,
Users
} from 'lucide-react';
import { ReactNode } from 'react';
import { TeamCard as UITeamCard } from '@/ui/TeamCard';
import React, { ReactNode } from 'react';
interface TeamCardProps {
name: string;
@@ -41,132 +31,37 @@ export function TeamCard({
categoryBadge,
region,
languagesContent,
statsContent: _statsContent,
onClick,
}: TeamCardProps) {
return (
<Stack 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 */}
<Stack p={5} pb={0}>
<Stack direction="row" align="start" gap={4}>
{/* Logo */}
<Stack
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} />
)}
<Stack position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="primary-accent/30" />
</Stack>
{/* Title & Badges */}
<Stack 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>
</Stack>
</Stack>
</Stack>
{/* Content */}
<Stack 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 && (
<Stack
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>
</Stack>
)}
{languagesContent}
</Stack>
)}
{/* Spacer */}
<Stack flexGrow={1} />
{/* Footer */}
<Stack
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>
</Stack>
</Stack>
</Card>
</Stack>
<UITeamCard
name={name}
description={description}
memberCount={memberCount}
isRecruiting={isRecruiting}
region={region}
onClick={onClick}
logo={
logo ? (
<Image
src={logo}
alt={name}
width={64}
height={64}
objectFit="cover"
/>
) : (
<PlaceholderImage size={64} />
)
}
badges={
<React.Fragment>
{performanceBadge}
{specializationContent}
{categoryBadge}
{languagesContent}
</React.Fragment>
}
/>
);
}

View File

@@ -1,10 +1,14 @@
'use client';
import { Badge } from '@/ui/Badge';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { ControlBar } from '@/ui/ControlBar';
import { SegmentedControl } from '@/ui/SegmentedControl';
import { Hash, LucideIcon, Percent, Search, Star, Trophy } from 'lucide-react';
import React from 'react';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
@@ -45,23 +49,24 @@ export function TeamFilter({
onSortChange,
}: TeamFilterProps) {
return (
<Stack mb={6} gap={4}>
{/* Search and Level Filter Row */}
<Stack direction="row" align="center" gap={4} wrap>
<Stack 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" />}
/>
</Stack>
{/* Level Filter */}
<Stack direction="row" align="center" gap={2} wrap>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '1.5rem' }}>
<ControlBar
leftContent={
<div style={{ maxWidth: '448px', width: '100%' }}>
<Input
type="text"
placeholder="Search teams..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
icon={<Icon icon={Search} size={5} intent="low" />}
fullWidth
/>
</div>
}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
<Button
variant={filterLevel === 'all' ? 'race-final' : 'secondary'}
variant={filterLevel === 'all' ? 'primary' : 'secondary'}
size="sm"
onClick={() => onFilterLevelChange('all')}
>
@@ -84,31 +89,21 @@ export function TeamFilter({
</Button>
);
})}
</Stack>
</Stack>
</div>
</ControlBar>
{/* Sort Options */}
<Stack direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-400">Sort by:</Text>
<Stack 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>
</Stack>
</Stack>
</Stack>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Text size="sm" variant="low">Sort by:</Text>
<SegmentedControl
options={SORT_OPTIONS.map(opt => ({
id: opt.id,
label: opt.label,
icon: <Icon icon={opt.icon} size={3.5} />
}))}
activeId={sortBy}
onChange={(id) => onSortChange(id as SortBy)}
/>
</div>
</div>
);
}

View File

@@ -1,73 +1,52 @@
'use client';
import { SkillLevelButton } from '@/components/drivers/SkillLevelButton';
import { TeamHeroSection as UiTeamHeroSection } from '@/components/teams/TeamHeroSection';
import { TeamHeroStats } from '@/components/teams/TeamHeroStats';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/primitives/Stack';
import { TeamHero } from '@/ui/TeamHero';
import { Text } from '@/ui/Text';
import {
Crown,
LucideIcon,
Plus,
Search,
Shield,
Star,
TrendingUp,
} from 'lucide-react';
import React from '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[] = [
const SKILL_LEVELS = [
{
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',
intent: 'warning' as const,
},
{
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',
intent: 'primary' as const,
},
{
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',
intent: 'telemetry' as const,
},
{
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',
intent: 'success' as const,
},
];
] as const;
interface TeamHeroSectionProps {
teams: TeamSummaryViewModel[];
@@ -87,24 +66,23 @@ export function TeamHeroSection({
onSkillLevelClick,
}: TeamHeroSectionProps) {
return (
<UiTeamHeroSection
<TeamHero
title={
<>
<React.Fragment>
Find Your
<Text color="text-purple-400"> Crew</Text>
</>
<Text as="span" variant="primary"> Crew</Text>
</React.Fragment>
}
description="Solo racing is great. Team racing is unforgettable. Join a team that matches your skill level and ambitions."
statsContent={
stats={
<TeamHeroStats teamCount={teams.length} recruitingCount={recruitingCount} />
}
actionsContent={
<>
actions={
<React.Fragment>
<Button
variant="primary"
onClick={onShowCreateForm}
icon={<Icon icon={Plus} size={4} />}
bg="bg-purple-600"
>
Create Team
</Button>
@@ -115,14 +93,14 @@ export function TeamHeroSection({
>
Browse Teams
</Button>
</>
</React.Fragment>
}
sideContent={
<>
<Text size="xs" color="text-gray-500" weight="medium" block mb={3} uppercase letterSpacing="0.05em">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Text size="xs" variant="low" weight="bold" uppercase>
Find Your Level
</Text>
<Stack gap={2}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{SKILL_LEVELS.map((level) => {
const count = teamsByLevel[level.id]?.length || 0;
@@ -131,16 +109,13 @@ export function TeamHeroSection({
key={level.id}
label={level.label}
icon={level.icon}
color={level.color}
bgColor={level.bgColor}
borderColor={level.borderColor}
count={count}
onClick={() => onSkillLevelClick(level.id)}
onClick={() => onSkillLevelClick(level.id as SkillLevel)}
/>
);
})}
</Stack>
</>
</div>
</div>
}
/>
);

View File

@@ -1,12 +1,7 @@
import { getMediaUrl } from '@/lib/utilities/media';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Podium, PodiumItem } from '@/ui/Podium';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { Crown, Trophy, Users } from 'lucide-react';
import { Podium } from '@/ui/Podium';
import React from 'react';
interface TeamPodiumProps {
teams: TeamSummaryViewModel[];
@@ -14,130 +9,20 @@ interface TeamPodiumProps {
}
export function TeamPodium({ teams, onClick }: TeamPodiumProps) {
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
if (teams.length < 3) return null;
const top3 = teams.slice(0, 3);
if (top3.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';
}
};
const entries = top3.map((team, index) => ({
name: team.name,
avatar: team.logoUrl || getMediaUrl('team-logo', team.id),
value: `${team.totalWins} Wins`,
position: (index + 1) as 1 | 2 | 3
}));
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
>
<Stack
bg={getBgColor(position)}
rounded="xl"
border={true}
borderColor="border-charcoal-outline"
p={4}
position="relative"
>
{/* Crown for 1st place */}
{position === 1 && (
<Stack position="absolute" top="-4" left="1/2" translateX="-1/2">
<Stack position="relative">
<Stack animate="pulse">
<Icon icon={Crown} size={8} color="text-warning-amber" />
</Stack>
<Stack position="absolute" inset="0" bg="bg-yellow-400" bgOpacity={0.3} blur="md" rounded="full" />
</Stack>
</Stack>
)}
{/* Team logo */}
<Stack 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"
/>
</Stack>
{/* 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>
</Stack>
</Button>
}
/>
);
})}
</Podium>
<Podium
title="Top 3 Teams"
entries={entries}
/>
);
}

View File

@@ -1,9 +1,11 @@
'use client';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/primitives/Stack';
import { Search } from 'lucide-react';
import React from 'react';
interface TeamSearchBarProps {
searchQuery: string;
@@ -12,18 +14,15 @@ interface TeamSearchBarProps {
export function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) {
return (
<Stack id="teams-list" mb={6}>
<Stack direction="row" gap={4} wrap>
<Stack 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)" />}
/>
</Stack>
</Stack>
</Stack>
<div style={{ marginBottom: '1.5rem' }}>
<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} intent="low" />}
fullWidth
/>
</div>
);
}

View File

@@ -1,12 +1,7 @@
import { getMediaUrl } from '@/lib/utilities/media';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Podium, PodiumItem } from '@/ui/Podium';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { Crown, Trophy, Users } from 'lucide-react';
import { Podium } from '@/ui/Podium';
import React from 'react';
interface TopThreePodiumProps {
teams: TeamSummaryViewModel[];
@@ -14,130 +9,20 @@ interface TopThreePodiumProps {
}
export function TopThreePodium({ teams, onClick }: TopThreePodiumProps) {
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
if (teams.length < 3) return null;
const top3 = teams.slice(0, 3);
if (top3.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';
}
};
const entries = top3.map((team, index) => ({
name: team.name,
avatar: team.logoUrl || getMediaUrl('team-logo', team.id),
value: `${team.totalWins} Wins`,
position: (index + 1) as 1 | 2 | 3
}));
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
>
<Stack
bg={getBgColor(position)}
rounded="xl"
border
borderColor="border-charcoal-outline"
p={4}
position="relative"
>
{/* Crown for 1st place */}
{position === 1 && (
<Stack position="absolute" top="-4" left="1/2" translateX="-1/2">
<Stack position="relative">
<Stack animate="pulse">
<Icon icon={Crown} size={8} color="var(--warning-amber)" />
</Stack>
<Stack position="absolute" inset="0" bg="bg-yellow-400" bgOpacity={0.3} blur="md" rounded="full" />
</Stack>
</Stack>
)}
{/* Team logo */}
<Stack 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"
/>
</Stack>
{/* 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>
</Stack>
</Button>
}
/>
);
})}
</Podium>
<Podium
title="Top 3 Teams"
entries={entries}
/>
);
}