This commit is contained in:
2026-01-05 19:35:49 +01:00
parent b4b915416b
commit d9e6151ae0
92 changed files with 10964 additions and 7893 deletions

View File

@@ -0,0 +1,130 @@
import React from 'react';
import { Search, Filter, Hash, Star, Trophy, Medal, Percent } from 'lucide-react';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
const SKILL_LEVELS: {
id: SkillLevel;
label: string;
color: string;
bgColor: string;
borderColor: string;
}[] = [
{ id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
{ id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
];
const SORT_OPTIONS: { id: SortBy; label: string; icon: React.ElementType }[] = [
{ id: 'rank', label: 'Rank', icon: Hash },
{ id: 'rating', label: 'Rating', icon: Star },
{ id: 'wins', label: 'Wins', icon: Trophy },
{ id: 'podiums', label: 'Podiums', icon: Medal },
{ id: 'winRate', label: 'Win Rate', icon: Percent },
];
interface DriverRankingsFilterProps {
searchQuery: string;
onSearchChange: (query: string) => void;
selectedSkill: 'all' | SkillLevel;
onSkillChange: (skill: 'all' | SkillLevel) => void;
sortBy: SortBy;
onSortChange: (sort: SortBy) => void;
showFilters: boolean;
onToggleFilters: () => void;
}
export default function DriverRankingsFilter({
searchQuery,
onSearchChange,
selectedSkill,
onSkillChange,
sortBy,
onSortChange,
showFilters,
onToggleFilters,
}: DriverRankingsFilterProps) {
return (
<div className="mb-6 space-y-4">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<Input
type="text"
placeholder="Search drivers by name or nationality..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-11"
/>
</div>
<Button
type="button"
variant="secondary"
onClick={onToggleFilters}
className="lg:hidden flex items-center gap-2"
>
<Filter className="w-4 h-4" />
Filters
</Button>
</div>
<div className={`flex flex-wrap gap-2 ${showFilters ? 'block' : 'hidden lg:flex'}`}>
<button
type="button"
onClick={() => onSkillChange('all')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all ${
selectedSkill === 'all'
? 'bg-primary-blue text-white'
: 'bg-iron-gray/50 text-gray-400 border border-charcoal-outline hover:text-white'
}`}
>
All Levels
</button>
{SKILL_LEVELS.map((level) => {
return (
<button
key={level.id}
type="button"
onClick={() => onSkillChange(level.id)}
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
selectedSkill === level.id
? `${level.bgColor} ${level.color} border ${level.borderColor}`
: 'bg-iron-gray/50 text-gray-400 border border-charcoal-outline hover:text-white'
}`}
>
{level.label}
</button>
);
})}
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Sort by:</span>
<div className="flex items-center gap-1 p-1 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
{SORT_OPTIONS.map((option) => {
const OptionIcon = option.icon;
return (
<button
key={option.id}
type="button"
onClick={() => onSortChange(option.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
sortBy === option.id
? 'bg-primary-blue text-white'
: 'text-gray-400 hover:text-white hover:bg-iron-gray'
}`}
>
<OptionIcon className="w-3.5 h-3.5" />
{option.label}
</button>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { Trophy, Medal, Crown } from 'lucide-react';
import Image from 'next/image';
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
interface DriverTopThreePodiumProps {
drivers: DriverLeaderboardItemViewModel[];
onDriverClick: (id: string) => void;
}
export default function DriverTopThreePodium({ drivers, onDriverClick }: DriverTopThreePodiumProps) {
if (drivers.length < 3) return null;
const top3 = drivers.slice(0, 3) as [DriverLeaderboardItemViewModel, DriverLeaderboardItemViewModel, DriverLeaderboardItemViewModel];
const podiumOrder: [DriverLeaderboardItemViewModel, DriverLeaderboardItemViewModel, DriverLeaderboardItemViewModel] = [
top3[1],
top3[0],
top3[2],
]; // 2nd, 1st, 3rd
const podiumHeights = ['h-32', 'h-40', 'h-24'];
const podiumColors = [
'from-gray-400/20 to-gray-500/10 border-gray-400/40',
'from-yellow-400/20 to-amber-500/10 border-yellow-400/40',
'from-amber-600/20 to-amber-700/10 border-amber-600/40',
];
const crownColors = ['text-gray-300', 'text-yellow-400', 'text-amber-600'];
const positions = [2, 1, 3];
return (
<div className="mb-10">
<div className="flex items-end justify-center gap-4 lg:gap-8">
{podiumOrder.map((driver, index) => {
const position = positions[index];
return (
<button
key={driver.id}
type="button"
onClick={() => onDriverClick(driver.id)}
className="flex flex-col items-center group"
>
{/* Driver Avatar & Info */}
<div className="relative mb-4">
{/* Crown for 1st place */}
{position === 1 && (
<div className="absolute -top-6 left-1/2 -translate-x-1/2 animate-bounce">
<Crown className="w-8 h-8 text-yellow-400 drop-shadow-[0_0_10px_rgba(250,204,21,0.5)]" />
</div>
)}
{/* Avatar */}
<div className={`relative ${position === 1 ? 'w-24 h-24 lg:w-28 lg:h-28' : 'w-20 h-20 lg:w-24 lg:h-24'} rounded-full overflow-hidden border-4 ${position === 1 ? 'border-yellow-400 shadow-[0_0_30px_rgba(250,204,21,0.3)]' : position === 2 ? 'border-gray-300' : 'border-amber-600'} group-hover:scale-105 transition-transform`}>
<Image
src={driver.avatarUrl}
alt={driver.name}
fill
className="object-cover"
/>
</div>
{/* Position badge */}
<div className={`absolute -bottom-2 left-1/2 -translate-x-1/2 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold bg-gradient-to-br ${podiumColors[index]} border-2 ${crownColors[index]}`}>
{position}
</div>
</div>
{/* Driver Name */}
<p className={`text-white font-semibold ${position === 1 ? 'text-lg' : 'text-base'} group-hover:text-primary-blue transition-colors mb-1`}>
{driver.name}
</p>
{/* Rating */}
<p className={`font-mono font-bold ${position === 1 ? 'text-xl text-yellow-400' : 'text-lg text-primary-blue'}`}>
{driver.rating.toLocaleString()}
</p>
{/* Stats */}
<div className="flex items-center gap-2 text-xs text-gray-500 mt-1">
<span className="flex items-center gap-1">
<Trophy className="w-3 h-3 text-performance-green" />
{driver.wins}
</span>
<span></span>
<span className="flex items-center gap-1">
<Medal className="w-3 h-3 text-warning-amber" />
{driver.podiums}
</span>
</div>
{/* Podium Stand */}
<div className={`mt-4 w-28 lg:w-36 ${podiumHeights[index]} rounded-t-lg bg-gradient-to-t ${podiumColors[index]} border-t border-x flex items-end justify-center pb-4`}>
<span className={`text-4xl lg:text-5xl font-black ${crownColors[index]}`}>
{position}
</span>
</div>
</button>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,119 @@
import React from 'react';
import { Search, Star, Trophy, Percent, Hash } from 'lucide-react';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
const SKILL_LEVELS: {
id: SkillLevel;
label: string;
color: string;
bgColor: string;
borderColor: string;
}[] = [
{ id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
{ id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
];
const SORT_OPTIONS: { id: SortBy; label: string; icon: React.ElementType }[] = [
{ 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 TeamRankingsFilterProps {
searchQuery: string;
onSearchChange: (query: string) => void;
filterLevel: SkillLevel | 'all';
onFilterLevelChange: (level: SkillLevel | 'all') => void;
sortBy: SortBy;
onSortChange: (sort: SortBy) => void;
}
export default function TeamRankingsFilter({
searchQuery,
onSearchChange,
filterLevel,
onFilterLevelChange,
sortBy,
onSortChange,
}: TeamRankingsFilterProps) {
return (
<div className="mb-6 space-y-4">
{/* Search and Level Filter Row */}
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<Input
type="text"
placeholder="Search teams..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-11"
/>
</div>
{/* Level Filter */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => onFilterLevelChange('all')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all ${
filterLevel === 'all'
? 'bg-purple-600 text-white'
: 'bg-iron-gray/50 text-gray-400 border border-charcoal-outline hover:text-white'
}`}
>
All Levels
</button>
{SKILL_LEVELS.map((level) => {
return (
<button
key={level.id}
type="button"
onClick={() => onFilterLevelChange(level.id)}
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
filterLevel === level.id
? `${level.bgColor} ${level.color} border ${level.borderColor}`
: 'bg-iron-gray/50 text-gray-400 border border-charcoal-outline hover:text-white'
}`}
>
{level.label}
</button>
);
})}
</div>
</div>
{/* Sort Options */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Sort by:</span>
<div className="flex items-center gap-1 p-1 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
{SORT_OPTIONS.map((option) => {
const OptionIcon = option.icon;
return (
<button
key={option.id}
type="button"
onClick={() => onSortChange(option.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
sortBy === option.id
? 'bg-purple-600 text-white'
: 'text-gray-400 hover:text-white hover:bg-iron-gray'
}`}
>
<OptionIcon className="w-3.5 h-3.5" />
{option.label}
</button>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -168,10 +168,10 @@ export default function AuthWorkflowMockup() {
>
<div className="text-center">
<p className="text-xs text-gray-400 mb-1">
Step {activeStep + 1}: {WORKFLOW_STEPS[activeStep].title}
Step {activeStep + 1}: {WORKFLOW_STEPS[activeStep]?.title || ''}
</p>
<p className="text-xs text-gray-500">
{WORKFLOW_STEPS[activeStep].description}
{WORKFLOW_STEPS[activeStep]?.description || ''}
</p>
</div>
</motion.div>

View File

@@ -0,0 +1,69 @@
'use client';
import { BarChart3 } from 'lucide-react';
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
const CATEGORIES = [
{ id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
{ id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400', bgColor: 'bg-orange-400/10', borderColor: 'border-orange-400/30' },
{ id: 'sprint', label: 'Sprint', color: 'text-red-400', bgColor: 'bg-red-400/10', borderColor: 'border-red-400/30' },
];
interface CategoryDistributionProps {
drivers: DriverLeaderboardItemViewModel[];
}
export function CategoryDistribution({ drivers }: CategoryDistributionProps) {
const distribution = CATEGORIES.map((category) => ({
...category,
count: drivers.filter((d) => d.category === category.id).length,
percentage: drivers.length > 0
? Math.round((drivers.filter((d) => d.category === category.id).length / drivers.length) * 100)
: 0,
}));
return (
<div className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-purple-400/10 border border-purple-400/20">
<BarChart3 className="w-5 h-5 text-purple-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Category Distribution</h2>
<p className="text-xs text-gray-500">Driver population by category</p>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{distribution.map((category) => (
<div
key={category.id}
className={`p-4 rounded-xl ${category.bgColor} border ${category.borderColor}`}
>
<div className="flex items-center justify-between mb-3">
<span className={`text-2xl font-bold ${category.color}`}>{category.count}</span>
</div>
<p className="text-white font-medium mb-1">{category.label}</p>
<div className="w-full h-2 rounded-full bg-deep-graphite/50 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
category.id === 'beginner' ? 'bg-green-400' :
category.id === 'intermediate' ? 'bg-primary-blue' :
category.id === 'advanced' ? 'bg-purple-400' :
category.id === 'pro' ? 'bg-yellow-400' :
category.id === 'endurance' ? 'bg-orange-400' :
'bg-red-400'
}`}
style={{ width: `${category.percentage}%` }}
/>
</div>
<p className="text-xs text-gray-500 mt-1">{category.percentage}% of drivers</p>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
'use client';
interface CircularProgressProps {
value: number;
max: number;
label: string;
color: string;
size?: number;
}
export function CircularProgress({ value, max, label, color, size = 80 }: CircularProgressProps) {
const percentage = Math.min((value / max) * 100, 100);
const strokeWidth = 6;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
return (
<div className="flex flex-col items-center">
<div className="relative" style={{ width: size, height: size }}>
<svg className="transform -rotate-90" width={size} height={size}>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
fill="transparent"
className="text-charcoal-outline"
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
fill="transparent"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
className={color}
style={{ transition: 'stroke-dashoffset 0.5s ease-in-out' }}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-lg font-bold text-white">{percentage.toFixed(0)}%</span>
</div>
</div>
<span className="text-xs text-gray-400 mt-2">{label}</span>
</div>
);
}

View File

@@ -43,12 +43,14 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic
// Load team data if available
if (profile.teamMemberships && profile.teamMemberships.length > 0) {
const currentTeam = profile.teamMemberships.find(m => m.isCurrent) || profile.teamMemberships[0];
setTeamData({
team: {
name: currentTeam.teamName,
tag: currentTeam.teamTag ?? ''
}
});
if (currentTeam) {
setTeamData({
team: {
name: currentTeam.teamName,
tag: currentTeam.teamTag ?? ''
}
});
}
}
} catch (error) {
console.error('Failed to load driver profile data:', error);

View File

@@ -0,0 +1,112 @@
'use client';
import { Trophy, Crown, Star, TrendingUp, Shield, Flag } from 'lucide-react';
import Image from 'next/image';
import { mediaConfig } from '@/lib/config/mediaConfig';
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', icon: Crown, color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30', description: 'Elite competition level' },
{ id: 'advanced', label: 'Advanced', icon: Star, color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30', description: 'Highly competitive' },
{ id: 'intermediate', label: 'Intermediate', icon: TrendingUp, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30', description: 'Developing skills' },
{ id: 'beginner', label: 'Beginner', icon: Shield, color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30', description: 'Learning the ropes' },
];
const CATEGORIES = [
{ id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
{ id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400', bgColor: 'bg-orange-400/10', borderColor: 'border-orange-400/30' },
{ id: 'sprint', label: 'Sprint', color: 'text-red-400', bgColor: 'bg-red-400/10', borderColor: 'border-red-400/30' },
];
interface FeaturedDriverCardProps {
driver: DriverLeaderboardItemViewModel;
position: number;
onClick: () => void;
}
export function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) {
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
const getBorderColor = (pos: number) => {
switch (pos) {
case 1: return 'border-yellow-400/50 hover:border-yellow-400';
case 2: return 'border-gray-300/50 hover:border-gray-300';
case 3: return 'border-amber-600/50 hover:border-amber-600';
default: return 'border-charcoal-outline hover:border-primary-blue';
}
};
const getMedalColor = (pos: number) => {
switch (pos) {
case 1: return 'text-yellow-400';
case 2: return 'text-gray-300';
case 3: return 'text-amber-600';
default: return 'text-gray-500';
}
};
return (
<button
type="button"
onClick={onClick}
className={`p-5 rounded-xl bg-iron-gray/60 border-2 ${getBorderColor(position)} transition-all duration-200 text-left group hover:scale-[1.02]`}
>
{/* Header with Position */}
<div className="flex items-start justify-between mb-4">
<div className={`flex h-10 w-10 items-center justify-center rounded-full ${position <= 3 ? 'bg-gradient-to-br from-yellow-400/20 to-amber-600/10' : 'bg-iron-gray'}`}>
{position <= 3 ? (
<Crown className={`w-5 h-5 ${getMedalColor(position)}`} />
) : (
<span className="text-lg font-bold text-gray-400">#{position}</span>
)}
</div>
<div className="flex gap-2">
{categoryConfig && (
<span className={`px-2 py-1 rounded-full text-[10px] font-medium ${categoryConfig.bgColor} ${categoryConfig.color} border ${categoryConfig.borderColor}`}>
{categoryConfig.label}
</span>
)}
<span className={`px-2 py-1 rounded-full text-[10px] font-medium ${levelConfig?.bgColor} ${levelConfig?.color} border ${levelConfig?.borderColor}`}>
{levelConfig?.label}
</span>
</div>
</div>
{/* Avatar & Name */}
<div className="flex items-center gap-4 mb-4">
<div className="relative w-16 h-16 rounded-full overflow-hidden border-2 border-charcoal-outline group-hover:border-primary-blue transition-colors">
<Image src={driver.avatarUrl || mediaConfig.avatars.defaultFallback} alt={driver.name} fill className="object-cover" />
</div>
<div>
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors">
{driver.name}
</h3>
<div className="flex items-center gap-2 text-sm text-gray-500">
<Flag className="w-3.5 h-3.5" />
{driver.nationality}
</div>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-3">
<div className="text-center p-2 rounded-lg bg-charcoal-outline/30">
<p className="text-lg font-bold text-primary-blue">{driver.rating.toLocaleString()}</p>
<p className="text-[10px] text-gray-500">Rating</p>
</div>
<div className="text-center p-2 rounded-lg bg-charcoal-outline/30">
<p className="text-lg font-bold text-performance-green">{driver.wins}</p>
<p className="text-[10px] text-gray-500">Wins</p>
</div>
<div className="text-center p-2 rounded-lg bg-charcoal-outline/30">
<p className="text-lg font-bold text-warning-amber">{driver.podiums}</p>
<p className="text-[10px] text-gray-500">Podiums</p>
</div>
</div>
</button>
);
}

View File

@@ -0,0 +1,27 @@
'use client';
interface HorizontalBarChartProps {
data: { label: string; value: number; color: string }[];
maxValue: number;
}
export function HorizontalBarChart({ data, maxValue }: HorizontalBarChartProps) {
return (
<div className="space-y-3">
{data.map((item) => (
<div key={item.label}>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-400">{item.label}</span>
<span className="text-white font-medium">{item.value}</span>
</div>
<div className="h-2 bg-charcoal-outline rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${item.color} transition-all duration-500 ease-out`}
style={{ width: `${Math.min((item.value / maxValue) * 100, 100)}%` }}
/>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,133 @@
'use client';
import { useRouter } from 'next/navigation';
import { Award, Crown, Flag, ChevronRight } from 'lucide-react';
import Image from 'next/image';
import Button from '@/components/ui/Button';
import { mediaConfig } from '@/lib/config/mediaConfig';
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
];
const CATEGORIES = [
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400' },
{ id: 'sprint', label: 'Sprint', color: 'text-red-400' },
];
interface LeaderboardPreviewProps {
drivers: DriverLeaderboardItemViewModel[];
onDriverClick: (id: string) => void;
}
export function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps) {
const router = useRouter();
const top5 = drivers.slice(0, 5);
const getMedalColor = (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 getMedalBg = (position: number) => {
switch (position) {
case 1: return 'bg-yellow-400/10 border-yellow-400/30';
case 2: return 'bg-gray-300/10 border-gray-300/30';
case 3: return 'bg-amber-600/10 border-amber-600/30';
default: return 'bg-iron-gray/50 border-charcoal-outline';
}
};
return (
<div className="mb-10">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-yellow-400/20 to-amber-600/10 border border-yellow-400/30">
<Award className="w-5 h-5 text-yellow-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Top Drivers</h2>
<p className="text-xs text-gray-500">Highest rated competitors</p>
</div>
</div>
<Button
variant="secondary"
onClick={() => router.push('/leaderboards/drivers')}
className="flex items-center gap-2 text-sm"
>
Full Rankings
<ChevronRight className="w-4 h-4" />
</Button>
</div>
<div className="rounded-xl bg-iron-gray/30 border border-charcoal-outline overflow-hidden">
<div className="divide-y divide-charcoal-outline/50">
{top5.map((driver, index) => {
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
const position = index + 1;
return (
<button
key={driver.id}
type="button"
onClick={() => onDriverClick(driver.id)}
className="flex items-center gap-4 px-4 py-3 w-full text-left hover:bg-iron-gray/30 transition-colors group"
>
{/* Position */}
<div className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold border ${getMedalBg(position)} ${getMedalColor(position)}`}>
{position <= 3 ? <Crown className="w-3.5 h-3.5" /> : position}
</div>
{/* Avatar */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={driver.avatarUrl || mediaConfig.avatars.defaultFallback} alt={driver.name} fill className="object-cover" />
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<p className="text-white font-medium truncate group-hover:text-primary-blue transition-colors">
{driver.name}
</p>
<div className="flex items-center gap-2 text-xs text-gray-500">
<Flag className="w-3 h-3" />
{driver.nationality}
{categoryConfig && (
<span className={categoryConfig.color}>{categoryConfig.label}</span>
)}
<span className={levelConfig?.color}>{levelConfig?.label}</span>
</div>
</div>
{/* Stats */}
<div className="flex items-center gap-4 text-sm">
<div className="text-center">
<p className="text-primary-blue font-mono font-semibold">{driver.rating.toLocaleString()}</p>
<p className="text-[10px] text-gray-500">Rating</p>
</div>
<div className="text-center">
<p className="text-performance-green font-mono font-semibold">{driver.wins}</p>
<p className="text-[10px] text-gray-500">Wins</p>
</div>
</div>
</button>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,74 @@
'use client';
import { Activity } from 'lucide-react';
import Image from 'next/image';
import { mediaConfig } from '@/lib/config/mediaConfig';
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
];
const CATEGORIES = [
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400' },
{ id: 'sprint', label: 'Sprint', color: 'text-red-400' },
];
interface RecentActivityProps {
drivers: DriverLeaderboardItemViewModel[];
onDriverClick: (id: string) => void;
}
export function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6);
return (
<div className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-performance-green/10 border border-performance-green/20">
<Activity className="w-5 h-5 text-performance-green" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Active Drivers</h2>
<p className="text-xs text-gray-500">Currently competing in leagues</p>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{activeDrivers.map((driver) => {
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
return (
<button
key={driver.id}
type="button"
onClick={() => onDriverClick(driver.id)}
className="p-3 rounded-xl bg-iron-gray/40 border border-charcoal-outline hover:border-performance-green/40 transition-all group text-center"
>
<div className="relative w-12 h-12 mx-auto rounded-full overflow-hidden border-2 border-charcoal-outline mb-2">
<Image src={driver.avatarUrl || mediaConfig.avatars.defaultFallback} alt={driver.name} fill className="object-cover" />
<div className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-performance-green border-2 border-iron-gray" />
</div>
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
{driver.name}
</p>
<div className="flex items-center justify-center gap-1 text-xs">
{categoryConfig && (
<span className={categoryConfig.color}>{categoryConfig.label}</span>
)}
<span className={levelConfig?.color}>{levelConfig?.label}</span>
</div>
</button>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,69 @@
'use client';
import { BarChart3 } from 'lucide-react';
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
const SKILL_LEVELS = [
{ id: 'pro', label: 'Pro', icon: BarChart3, color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
{ id: 'advanced', label: 'Advanced', icon: BarChart3, color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
{ id: 'intermediate', label: 'Intermediate', icon: BarChart3, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
{ id: 'beginner', label: 'Beginner', icon: BarChart3, color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
];
interface SkillDistributionProps {
drivers: DriverLeaderboardItemViewModel[];
}
export function SkillDistribution({ drivers }: SkillDistributionProps) {
const distribution = SKILL_LEVELS.map((level) => ({
...level,
count: drivers.filter((d) => d.skillLevel === level.id).length,
percentage: drivers.length > 0
? Math.round((drivers.filter((d) => d.skillLevel === level.id).length / drivers.length) * 100)
: 0,
}));
return (
<div className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-neon-aqua/10 border border-neon-aqua/20">
<BarChart3 className="w-5 h-5 text-neon-aqua" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Skill Distribution</h2>
<p className="text-xs text-gray-500">Driver population by skill level</p>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{distribution.map((level) => {
const Icon = level.icon;
return (
<div
key={level.id}
className={`p-4 rounded-xl ${level.bgColor} border ${level.borderColor}`}
>
<div className="flex items-center justify-between mb-3">
<Icon className={`w-5 h-5 ${level.color}`} />
<span className={`text-2xl font-bold ${level.color}`}>{level.count}</span>
</div>
<p className="text-white font-medium mb-1">{level.label}</p>
<div className="w-full h-2 rounded-full bg-deep-graphite/50 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
level.id === 'pro' ? 'bg-yellow-400' :
level.id === 'advanced' ? 'bg-purple-400' :
level.id === 'intermediate' ? 'bg-primary-blue' :
'bg-green-400'
}`}
style={{ width: `${level.percentage}%` }}
/>
</div>
<p className="text-xs text-gray-500 mt-1">{level.percentage}% of drivers</p>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -19,7 +19,13 @@ interface RaceCardProps {
}
export function RaceCard({ race, onClick, className }: RaceCardProps) {
const config = raceStatusConfig[race.status as keyof typeof raceStatusConfig];
const config = raceStatusConfig[race.status as keyof typeof raceStatusConfig] || {
border: 'border-charcoal-outline',
bg: 'bg-charcoal-outline',
color: 'text-gray-400',
icon: () => null,
label: 'Scheduled',
};
return (
<div
@@ -70,7 +76,7 @@ export function RaceCard({ race, onClick, className }: RaceCardProps) {
{/* Status Badge */}
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full ${config.bg} ${config.border} border`}>
<config.icon className={`w-3.5 h-3.5 ${config.color}`} />
{config.icon && <config.icon className={`w-3.5 h-3.5 ${config.color}`} />}
<span className={`text-xs font-medium ${config.color}`}>
{config.label}
</span>

View File

@@ -0,0 +1,151 @@
'use client';
import { X, Filter, Search } from 'lucide-react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
export type StatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
interface RaceFilterModalProps {
isOpen: boolean;
onClose: () => void;
statusFilter: StatusFilter;
setStatusFilter: (filter: StatusFilter) => void;
leagueFilter: string;
setLeagueFilter: (filter: string) => void;
timeFilter: TimeFilter;
setTimeFilter: (filter: TimeFilter) => void;
searchQuery: string;
setSearchQuery: (query: string) => void;
leagues: Array<{ id: string; name: string }>;
showSearch?: boolean;
showTimeFilter?: boolean;
}
export function RaceFilterModal({
isOpen,
onClose,
statusFilter,
setStatusFilter,
leagueFilter,
setLeagueFilter,
timeFilter,
setTimeFilter,
searchQuery,
setSearchQuery,
leagues,
showSearch = true,
showTimeFilter = true,
}: RaceFilterModalProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" onClick={onClose}>
<div className="w-full max-w-md" onClick={(e) => e.stopPropagation()}>
<Card className="!p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-primary-blue" />
<h3 className="text-lg font-semibold text-white">Filters</h3>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-white">
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
{/* Search */}
{showSearch && (
<div>
<label className="block text-sm text-gray-400 mb-2">Search</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Track, car, or league..."
className="w-full pl-10 pr-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-blue"
/>
</div>
</div>
)}
{/* Time Filter */}
{showTimeFilter && (
<div>
<label className="block text-sm text-gray-400 mb-2">Time</label>
<div className="flex gap-2">
{(['upcoming', 'live', 'past', 'all'] as TimeFilter[]).map(filter => (
<button
key={filter}
onClick={() => setTimeFilter(filter)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all ${
timeFilter === filter
? 'bg-primary-blue text-white'
: 'bg-deep-graphite text-gray-400 hover:text-white'
}`}
>
{filter === 'live' && <span className="inline-block w-2 h-2 bg-performance-green rounded-full mr-1 animate-pulse" />}
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</button>
))}
</div>
</div>
)}
{/* Status Filter */}
<div>
<label className="block text-sm text-gray-400 mb-2">Status</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as StatusFilter)}
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="all">All Statuses</option>
<option value="scheduled">Scheduled</option>
<option value="running">Live</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
{/* League Filter */}
<div>
<label className="block text-sm text-gray-400 mb-2">League</label>
<select
value={leagueFilter}
onChange={(e) => setLeagueFilter(e.target.value)}
className="w-full px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="all">All Leagues</option>
{leagues.map(league => (
<option key={league.id} value={league.id}>
{league.name}
</option>
))}
</select>
</div>
{/* Clear Filters */}
{(statusFilter !== 'all' || leagueFilter !== 'all' || searchQuery || (showTimeFilter && timeFilter !== 'upcoming')) && (
<Button
variant="secondary"
onClick={() => {
setStatusFilter('all');
setLeagueFilter('all');
setSearchQuery('');
if (showTimeFilter) setTimeFilter('upcoming');
}}
className="w-full"
>
Clear All Filters
</Button>
)}
</div>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,122 @@
'use client';
import { UserPlus, UserMinus, CheckCircle2, PlayCircle, XCircle } from 'lucide-react';
import Button from '@/components/ui/Button';
interface RaceJoinButtonProps {
raceStatus: 'scheduled' | 'running' | 'completed' | 'cancelled';
isUserRegistered: boolean;
canRegister: boolean;
onRegister: () => void;
onWithdraw: () => void;
onCancel: () => void;
onReopen?: () => void;
onEndRace?: () => void;
canReopenRace?: boolean;
isOwnerOrAdmin?: boolean;
isLoading?: {
register?: boolean;
withdraw?: boolean;
cancel?: boolean;
reopen?: boolean;
};
}
export function RaceJoinButton({
raceStatus,
isUserRegistered,
canRegister,
onRegister,
onWithdraw,
onCancel,
onReopen,
onEndRace,
canReopenRace = false,
isOwnerOrAdmin = false,
isLoading = {},
}: RaceJoinButtonProps) {
// Show registration button for scheduled races
if (raceStatus === 'scheduled') {
if (canRegister && !isUserRegistered) {
return (
<Button
variant="primary"
className="w-full flex items-center justify-center gap-2"
onClick={onRegister}
disabled={isLoading.register}
>
<UserPlus className="w-4 h-4" />
{isLoading.register ? 'Registering...' : 'Register for Race'}
</Button>
);
}
if (isUserRegistered) {
return (
<>
<div className="flex items-center gap-2 px-4 py-3 bg-performance-green/10 border border-performance-green/30 rounded-lg text-performance-green">
<CheckCircle2 className="w-5 h-5" />
<span className="font-medium">You're Registered</span>
</div>
<Button
variant="secondary"
className="w-full flex items-center justify-center gap-2"
onClick={onWithdraw}
disabled={isLoading.withdraw}
>
<UserMinus className="w-4 h-4" />
{isLoading.withdraw ? 'Withdrawing...' : 'Withdraw'}
</Button>
</>
);
}
// Show cancel button for owners/admins
if (isOwnerOrAdmin) {
return (
<Button
variant="secondary"
className="w-full flex items-center justify-center gap-2"
onClick={onCancel}
disabled={isLoading.cancel}
>
<XCircle className="w-4 h-4" />
{isLoading.cancel ? 'Cancelling...' : 'Cancel Race'}
</Button>
);
}
return null;
}
// Show end race button for running races (owners/admins only)
if (raceStatus === 'running' && isOwnerOrAdmin && onEndRace) {
return (
<Button
variant="primary"
className="w-full flex items-center justify-center gap-2"
onClick={onEndRace}
>
<CheckCircle2 className="w-4 h-4" />
End Race & Process Results
</Button>
);
}
// Show reopen button for completed/cancelled races (owners/admins only)
if ((raceStatus === 'completed' || raceStatus === 'cancelled') && canReopenRace && isOwnerOrAdmin && onReopen) {
return (
<Button
variant="secondary"
className="w-full flex items-center justify-center gap-2"
onClick={onReopen}
disabled={isLoading.reopen}
>
<PlayCircle className="w-4 h-4" />
{isLoading.reopen ? 'Re-opening...' : 'Re-open Race'}
</Button>
);
}
return null;
}

View File

@@ -0,0 +1,84 @@
'use client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface RacePaginationProps {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
onPageChange: (page: number) => void;
}
export function RacePagination({
currentPage,
totalPages,
totalItems,
itemsPerPage,
onPageChange,
}: RacePaginationProps) {
if (totalPages <= 1) return null;
const startItem = ((currentPage - 1) * itemsPerPage) + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
const getPageNumbers = () => {
const pages: number[] = [];
if (totalPages <= 5) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
if (currentPage <= 3) {
return [1, 2, 3, 4, 5];
}
if (currentPage >= totalPages - 2) {
return [totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
}
return [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2];
};
return (
<div className="flex items-center justify-between pt-4">
<p className="text-sm text-gray-500">
Showing {startItem}{endItem} of {totalItems}
</p>
<div className="flex items-center gap-2">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="p-2 rounded-lg border border-charcoal-outline text-gray-400 hover:text-white hover:border-primary-blue disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-1">
{getPageNumbers().map(pageNum => (
<button
key={pageNum}
onClick={() => onPageChange(pageNum)}
className={`w-10 h-10 rounded-lg text-sm font-medium transition-colors ${
currentPage === pageNum
? 'bg-primary-blue text-white'
: 'text-gray-400 hover:text-white hover:bg-iron-gray'
}`}
>
{pageNum}
</button>
))}
</div>
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="p-2 rounded-lg border border-charcoal-outline text-gray-400 hover:text-white hover:border-primary-blue disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,44 @@
'use client';
import { useState } from 'react';
export type StewardingTab = 'pending' | 'resolved' | 'penalties';
interface StewardingTabsProps {
activeTab: StewardingTab;
onTabChange: (tab: StewardingTab) => void;
pendingCount: number;
}
export function StewardingTabs({ activeTab, onTabChange, pendingCount }: StewardingTabsProps) {
const tabs: Array<{ id: StewardingTab; label: string }> = [
{ id: 'pending', label: 'Pending' },
{ id: 'resolved', label: 'Resolved' },
{ id: 'penalties', label: 'Penalties' },
];
return (
<div className="border-b border-charcoal-outline">
<div className="flex gap-4">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`pb-3 px-1 font-medium transition-colors ${
activeTab === tab.id
? 'text-primary-blue border-b-2 border-primary-blue'
: 'text-gray-400 hover:text-white'
}`}
>
{tab.label}
{tab.id === 'pending' && pendingCount > 0 && (
<span className="ml-2 px-2 py-0.5 text-xs bg-warning-amber/20 text-warning-amber rounded-full">
{pendingCount}
</span>
)}
</button>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,177 @@
'use client';
import {
Users,
Search,
Plus,
Crown,
Star,
TrendingUp,
Shield,
UserPlus,
} from 'lucide-react';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
interface SkillLevelConfig {
id: SkillLevel;
label: string;
icon: React.ElementType;
color: string;
bgColor: string;
borderColor: string;
description: string;
}
const SKILL_LEVELS: SkillLevelConfig[] = [
{
id: 'pro',
label: 'Pro',
icon: Crown,
color: 'text-yellow-400',
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-400/10',
borderColor: 'border-purple-400/30',
description: 'Competitive racing, high consistency',
},
{
id: 'intermediate',
label: 'Intermediate',
icon: TrendingUp,
color: 'text-primary-blue',
bgColor: 'bg-primary-blue/10',
borderColor: 'border-primary-blue/30',
description: 'Growing skills, regular practice',
},
{
id: 'beginner',
label: 'Beginner',
icon: Shield,
color: 'text-green-400',
bgColor: 'bg-green-400/10',
borderColor: 'border-green-400/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 default function TeamHeroSection({
teams,
teamsByLevel,
recruitingCount,
onShowCreateForm,
onBrowseTeams,
onSkillLevelClick,
}: TeamHeroSectionProps) {
return (
<div className="relative mb-10 overflow-hidden">
{/* Main Hero Card */}
<div className="relative py-12 px-8 rounded-2xl bg-gradient-to-br from-purple-900/30 via-iron-gray/80 to-deep-graphite border border-purple-500/20">
{/* Background decorations */}
<div className="absolute top-0 right-0 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl" />
<div className="absolute bottom-0 left-1/4 w-64 h-64 bg-neon-aqua/5 rounded-full blur-3xl" />
<div className="absolute top-1/2 right-1/4 w-48 h-48 bg-yellow-400/5 rounded-full blur-2xl" />
<div className="relative z-10">
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-8">
<div className="max-w-xl">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-xs font-medium mb-4">
<Users className="w-3.5 h-3.5" />
Team Racing
</div>
<Heading level={1} className="text-4xl lg:text-5xl mb-4">
Find Your
<span className="text-purple-400"> Crew</span>
</Heading>
<p className="text-gray-400 text-lg leading-relaxed mb-6">
Solo racing is great. Team racing is unforgettable. Join a team that matches your skill level and ambitions.
</p>
{/* Quick Stats */}
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
<Users className="w-4 h-4 text-purple-400" />
<span className="text-white font-semibold">{teams.length}</span>
<span className="text-gray-500 text-sm">Teams</span>
</div>
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
<UserPlus className="w-4 h-4 text-performance-green" />
<span className="text-white font-semibold">{recruitingCount}</span>
<span className="text-gray-500 text-sm">Recruiting</span>
</div>
</div>
{/* CTA Buttons */}
<div className="flex flex-wrap gap-3">
<Button
variant="primary"
onClick={onShowCreateForm}
className="flex items-center gap-2 px-5 py-2.5 bg-purple-600 hover:bg-purple-500"
>
<Plus className="w-4 h-4" />
Create Team
</Button>
<Button
variant="secondary"
onClick={onBrowseTeams}
className="flex items-center gap-2"
>
<Search className="w-4 h-4" />
Browse Teams
</Button>
</div>
</div>
{/* Skill Level Quick Nav */}
<div className="lg:w-72">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-3">Find Your Level</p>
<div className="space-y-2">
{SKILL_LEVELS.map((level) => {
const LevelIcon = level.icon;
const count = teamsByLevel[level.id]?.length || 0;
return (
<button
key={level.id}
type="button"
onClick={() => onSkillLevelClick(level.id)}
className={`w-full flex items-center justify-between p-3 rounded-lg ${level.bgColor} border ${level.borderColor} hover:scale-[1.02] transition-all duration-200`}
>
<div className="flex items-center gap-2">
<LevelIcon className={`w-4 h-4 ${level.color}`} />
<span className="text-white font-medium">{level.label}</span>
</div>
<span className="text-gray-400 text-sm">{count} teams</span>
</button>
);
})}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
'use client';
import { Search } from 'lucide-react';
import Input from '@/components/ui/Input';
interface TeamSearchBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
}
export default function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) {
return (
<div id="teams-list" className="mb-6 scroll-mt-8">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<Input
type="text"
placeholder="Search teams by name, description, region, or language..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-11"
/>
</div>
</div>
</div>
);
}