extract components from website

This commit is contained in:
2025-12-21 13:55:31 +01:00
parent 13d8563feb
commit b52474d792
65 changed files with 3234 additions and 1361 deletions

View File

@@ -27,6 +27,7 @@ import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import { useAuth } from '@/lib/auth/AuthContext';
import AuthWorkflowMockup from '@/components/auth/AuthWorkflowMockup';
import UserRolesPreview from '@/components/auth/UserRolesPreview';
interface FormErrors {
email?: string;
@@ -34,26 +35,6 @@ interface FormErrors {
submit?: string;
}
const USER_ROLES = [
{
icon: Car,
title: 'Driver',
description: 'Race, track stats, join teams',
color: 'primary-blue',
},
{
icon: Trophy,
title: 'League Admin',
description: 'Organize leagues and events',
color: 'performance-green',
},
{
icon: Users,
title: 'Team Manager',
description: 'Manage team and drivers',
color: 'purple-400',
},
];
export default function LoginPage() {
const router = useRouter();
@@ -167,25 +148,7 @@ export default function LoginPage() {
</p>
{/* Role Cards */}
<div className="space-y-3 mb-8">
{USER_ROLES.map((role, index) => (
<motion.div
key={role.title}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center gap-4 p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline"
>
<div className={`w-10 h-10 rounded-lg bg-${role.color}/20 flex items-center justify-center`}>
<role.icon className={`w-5 h-5 text-${role.color}`} />
</div>
<div>
<h4 className="text-white font-medium">{role.title}</h4>
<p className="text-sm text-gray-500">{role.description}</p>
</div>
</motion.div>
))}
</div>
<UserRolesPreview variant="full" />
{/* Workflow Mockup */}
<AuthWorkflowMockup />
@@ -365,19 +328,7 @@ export default function LoginPage() {
</p>
{/* Mobile Role Info */}
<div className="mt-8 lg:hidden">
<p className="text-center text-xs text-gray-500 mb-4">One account for all roles</p>
<div className="flex justify-center gap-6">
{USER_ROLES.map((role) => (
<div key={role.title} className="flex flex-col items-center">
<div className={`w-8 h-8 rounded-lg bg-${role.color}/20 flex items-center justify-center mb-1`}>
<role.icon className={`w-4 h-4 text-${role.color}`} />
</div>
<span className="text-xs text-gray-500">{role.title}</span>
</div>
))}
</div>
</div>
<UserRolesPreview variant="compact" />
</div>
</div>
</main>

View File

@@ -23,58 +23,15 @@ import {
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { StatCard } from '@/components/dashboard/StatCard';
import { LeagueStandingItem } from '@/components/dashboard/LeagueStandingItem';
import { UpcomingRaceItem } from '@/components/dashboard/UpcomingRaceItem';
import { FriendItem } from '@/components/dashboard/FriendItem';
import { FeedItemRow } from '@/components/dashboard/FeedItemRow';
import { useDashboardOverview } from '@/hooks/useDashboardService';
// Helper functions
function getCountryFlag(countryCode: string): string {
const code = countryCode.toUpperCase();
if (code.length === 2) {
const codePoints = [...code].map(char => 127397 + char.charCodeAt(0));
return String.fromCodePoint(...codePoints);
}
return '🏁';
}
function timeUntil(date: Date): string {
const now = new Date();
const diffMs = date.getTime() - now.getTime();
if (diffMs < 0) return 'Started';
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) {
return `${diffDays}d ${diffHours % 24}h`;
}
if (diffHours > 0) {
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
return `${diffHours}h ${diffMinutes}m`;
}
const diffMinutes = Math.floor(diffMs / (1000 * 60));
return `${diffMinutes}m`;
}
function timeAgo(timestamp: Date | string): string {
const time = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const diffMs = Date.now() - time.getTime();
const diffMinutes = Math.floor(diffMs / 60000);
if (diffMinutes < 1) return 'Just now';
if (diffMinutes < 60) return `${diffMinutes}m ago`;
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
function getGreeting(): string {
const hour = new Date().getHours();
if (hour < 12) return 'Good morning';
if (hour < 18) return 'Good afternoon';
return 'Good evening';
}
import { getCountryFlag } from '@/lib/utilities/country';
import { getGreeting, timeUntil, timeAgo } from '@/lib/utilities/time';
import { DashboardFeedItemSummaryViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
@@ -176,50 +133,10 @@ export default function DashboardPage() {
{/* Quick Stats Row */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-performance-green/20">
<Trophy className="w-5 h-5 text-performance-green" />
</div>
<div>
<p className="text-2xl font-bold text-white">{wins}</p>
<p className="text-xs text-gray-500">Wins</p>
</div>
</div>
</div>
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-warning-amber/20">
<Medal className="w-5 h-5 text-warning-amber" />
</div>
<div>
<p className="text-2xl font-bold text-white">{podiums}</p>
<p className="text-xs text-gray-500">Podiums</p>
</div>
</div>
</div>
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/20">
<Target className="w-5 h-5 text-primary-blue" />
</div>
<div>
<p className="text-2xl font-bold text-white">{consistency}%</p>
<p className="text-xs text-gray-500">Consistency</p>
</div>
</div>
</div>
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/20">
<Users className="w-5 h-5 text-purple-400" />
</div>
<div>
<p className="text-2xl font-bold text-white">{activeLeaguesCount}</p>
<p className="text-xs text-gray-500">Active Leagues</p>
</div>
</div>
</div>
<StatCard icon={Trophy} value={wins} label="Wins" color="bg-performance-green/20 text-performance-green" />
<StatCard icon={Medal} value={podiums} label="Podiums" color="bg-warning-amber/20 text-warning-amber" />
<StatCard icon={Target} value={`${consistency}%`} label="Consistency" color="bg-primary-blue/20 text-primary-blue" />
<StatCard icon={Users} value={activeLeaguesCount} label="Active Leagues" color="bg-purple-500/20 text-purple-400" />
</div>
</div>
</section>
@@ -300,38 +217,14 @@ export default function DashboardPage() {
</div>
<div className="space-y-3">
{leagueStandingsSummaries.map(({ leagueId, leagueName, position, points, totalDrivers }) => (
<Link
<LeagueStandingItem
key={leagueId}
href={`/leagues/${leagueId}/standings`}
className="flex items-center gap-4 p-4 rounded-xl bg-deep-graphite border border-charcoal-outline hover:border-primary-blue/30 transition-colors group"
>
<div className={`flex h-12 w-12 items-center justify-center rounded-xl font-bold text-xl ${
position === 1 ? 'bg-yellow-400/20 text-yellow-400' :
position === 2 ? 'bg-gray-300/20 text-gray-300' :
position === 3 ? 'bg-orange-400/20 text-orange-400' :
'bg-iron-gray text-gray-400'
}`}>
{position > 0 ? `P${position}` : '-'}
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">
{leagueName}
</p>
<p className="text-sm text-gray-500">
{points} points {totalDrivers} drivers
</p>
</div>
<div className="flex items-center gap-2">
{position <= 3 && position > 0 && (
<Crown className={`w-5 h-5 ${
position === 1 ? 'text-yellow-400' :
position === 2 ? 'text-gray-300' :
'text-orange-400'
}`} />
)}
<ChevronRight className="w-5 h-5 text-gray-500 group-hover:text-primary-blue transition-colors" />
</div>
</Link>
leagueId={leagueId}
leagueName={leagueName}
position={position}
points={points}
totalDrivers={totalDrivers}
/>
))}
</div>
</Card>
@@ -376,30 +269,16 @@ export default function DashboardPage() {
</div>
{upcomingRaces.length > 0 ? (
<div className="space-y-3">
{upcomingRaces.slice(0, 5).map((race) => {
const isMyRace = race.isMyLeague;
return (
<Link
key={race.id}
href={`/races/${race.id}`}
className="block p-3 rounded-lg bg-deep-graphite border border-charcoal-outline hover:border-primary-blue/30 transition-colors"
>
<div className="flex items-start justify-between gap-2 mb-2">
<p className="text-white font-medium text-sm truncate">{race.track}</p>
{isMyRace && (
<span className="flex-shrink-0 w-2 h-2 rounded-full bg-performance-green" title="Your league" />
)}
</div>
<p className="text-xs text-gray-500 truncate mb-2">{race.car}</p>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-400">
{race.scheduledAt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
<span className="text-primary-blue font-medium">{timeUntil(race.scheduledAt)}</span>
</div>
</Link>
);
})}
{upcomingRaces.slice(0, 5).map((race) => (
<UpcomingRaceItem
key={race.id}
id={race.id}
track={race.track}
car={race.car}
scheduledAt={race.scheduledAt}
isMyLeague={race.isMyLeague}
/>
))}
</div>
) : (
<p className="text-gray-500 text-sm text-center py-4">No upcoming races</p>
@@ -418,25 +297,13 @@ export default function DashboardPage() {
{friends.length > 0 ? (
<div className="space-y-2">
{friends.slice(0, 6).map((friend) => (
<Link
<FriendItem
key={friend.id}
href={`/drivers/${friend.id}`}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite transition-colors"
>
<div className="w-9 h-9 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
<Image
src={friend.avatarUrl}
alt={friend.name}
width={36}
height={36}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">{friend.name}</p>
<p className="text-xs text-gray-500">{getCountryFlag(friend.country)}</p>
</div>
</Link>
id={friend.id}
name={friend.name}
avatarUrl={friend.avatarUrl}
country={friend.country}
/>
))}
{friends.length > 6 && (
<Link
@@ -465,40 +332,3 @@ export default function DashboardPage() {
</main>
);
}
// Feed Item Row Component
function FeedItemRow({ item }: { item: DashboardFeedItemSummaryViewModel }) {
const getActivityIcon = (type: string) => {
if (type.includes('win')) return { icon: Trophy, color: 'text-yellow-400 bg-yellow-400/10' };
if (type.includes('podium')) return { icon: Medal, color: 'text-warning-amber bg-warning-amber/10' };
if (type.includes('join')) return { icon: UserPlus, color: 'text-performance-green bg-performance-green/10' };
if (type.includes('friend')) return { icon: Heart, color: 'text-pink-400 bg-pink-400/10' };
if (type.includes('league')) return { icon: Flag, color: 'text-primary-blue bg-primary-blue/10' };
if (type.includes('race')) return { icon: Play, color: 'text-red-400 bg-red-400/10' };
return { icon: Activity, color: 'text-gray-400 bg-gray-400/10' };
};
const { icon: Icon, color } = getActivityIcon(item.type);
return (
<div className="flex gap-3 p-3 rounded-lg bg-deep-graphite/50 border border-charcoal-outline">
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${color} flex-shrink-0`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-white">{item.headline}</p>
{item.body && (
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{item.body}</p>
)}
<p className="text-xs text-gray-500 mt-1">{timeAgo(item.timestamp)}</p>
</div>
{item.ctaHref && (
<Link href={item.ctaHref} className="flex-shrink-0">
<Button variant="secondary" className="text-xs px-3 py-1.5">
{item.ctaLabel || 'View'}
</Button>
</Link>
)}
</div>
);
}

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Card from '@/components/ui/Card';
import PointsTable from '@/components/leagues/PointsTable';
import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueDetailPageViewModel } from '@/lib/view-models/LeagueDetailPageViewModel';
@@ -124,49 +125,7 @@ export default function LeagueRulebookPage() {
</div>
{/* Points Table */}
<Card>
<h2 className="text-lg font-semibold text-white mb-4">Points Distribution</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-charcoal-outline">
<th className="text-left py-3 px-4 font-medium text-gray-400">Position</th>
<th className="text-right py-3 px-4 font-medium text-gray-400">Points</th>
</tr>
</thead>
<tbody>
{positionPoints.map(({ position, points }) => (
<tr
key={position}
className={`border-b border-charcoal-outline/50 transition-colors hover:bg-iron-gray/30 ${
position <= 3 ? 'bg-iron-gray/20' : ''
}`}
>
<td className="py-3 px-4">
<div className="flex items-center gap-3">
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold ${
position === 1 ? 'bg-yellow-500 text-black' :
position === 2 ? 'bg-gray-400 text-black' :
position === 3 ? 'bg-amber-600 text-white' :
'bg-charcoal-outline text-white'
}`}>
{position}
</div>
<span className="text-white font-medium">
{position === 1 ? '1st' : position === 2 ? '2nd' : position === 3 ? '3rd' : `${position}th`}
</span>
</div>
</td>
<td className="py-3 px-4 text-right">
<span className="text-white font-semibold tabular-nums">{points}</span>
<span className="text-gray-500 ml-1">pts</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
<PointsTable points={positionPoints} />
{/* Bonus Points */}
{primaryChampionship?.bonusSummary && primaryChampionship.bonusSummary.length > 0 && (

View File

@@ -1,8 +1,7 @@
'use client';
import { ReadonlyLeagueInfo } from '@/components/leagues/ReadonlyLeagueInfo';
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
import Button from '@/components/ui/Button';
import LeagueOwnershipTransfer from '@/components/leagues/LeagueOwnershipTransfer';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
@@ -22,9 +21,6 @@ export default function LeagueSettingsPage() {
const [settings, setSettings] = useState<LeagueSettingsViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const [showTransferDialog, setShowTransferDialog] = useState(false);
const [selectedNewOwner, setSelectedNewOwner] = useState<string>('');
const [transferring, setTransferring] = useState(false);
const router = useRouter();
useEffect(() => {
@@ -58,20 +54,12 @@ export default function LeagueSettingsPage() {
const ownerSummary = settings?.owner || null;
const handleTransferOwnership = async () => {
if (!selectedNewOwner || !settings) return;
setTransferring(true);
const handleTransferOwnership = async (newOwnerId: string) => {
try {
await leagueSettingsService.transferOwnership(leagueId, currentDriverId, selectedNewOwner);
setShowTransferDialog(false);
await leagueSettingsService.transferOwnership(leagueId, currentDriverId, newOwnerId);
router.refresh();
} catch (err) {
console.error('Failed to transfer ownership:', err);
alert(err instanceof Error ? err.message : 'Failed to transfer ownership');
} finally {
setTransferring(false);
throw err; // Let the component handle the error
}
};
@@ -128,76 +116,11 @@ export default function LeagueSettingsPage() {
<div className="space-y-4">
<ReadonlyLeagueInfo league={settings.league} configForm={settings.config} />
{/* League Owner - Compact */}
<div className="rounded-xl border border-charcoal-outline bg-gradient-to-br from-iron-gray/40 to-iron-gray/20 p-5">
<h3 className="text-sm font-semibold text-gray-400 mb-3">League Owner</h3>
{ownerSummary ? (
<DriverSummaryPill
driver={ownerSummary.driver}
rating={ownerSummary.rating}
rank={ownerSummary.rank}
/>
) : (
<p className="text-sm text-gray-500">Loading owner details...</p>
)}
</div>
{/* Transfer Ownership - Owner Only */}
{settings.league.ownerId === currentDriverId && settings.members.length > 0 && (
<div className="rounded-xl border border-charcoal-outline bg-gradient-to-br from-iron-gray/40 to-iron-gray/20 p-5">
<div className="flex items-center gap-2 mb-3">
<UserCog className="w-4 h-4 text-gray-400" />
<h3 className="text-sm font-semibold text-gray-400">Transfer Ownership</h3>
</div>
<p className="text-xs text-gray-500 mb-4">
Transfer league ownership to another active member. You will become an admin.
</p>
{!showTransferDialog ? (
<Button
variant="secondary"
onClick={() => setShowTransferDialog(true)}
>
Transfer Ownership
</Button>
) : (
<div className="space-y-3">
<select
value={selectedNewOwner}
onChange={(e) => setSelectedNewOwner(e.target.value)}
className="w-full rounded-lg border border-charcoal-outline bg-charcoal-card px-3 py-2 text-sm text-white focus:border-primary-blue focus:outline-none"
>
<option value="">Select new owner...</option>
{settings.members.map((member) => (
<option key={member.driver.id} value={member.driver.id}>
{member.driver.name}
</option>
))}
</select>
<div className="flex gap-2">
<Button
variant="primary"
onClick={handleTransferOwnership}
disabled={!selectedNewOwner || transferring}
>
{transferring ? 'Transferring...' : 'Confirm Transfer'}
</Button>
<Button
variant="secondary"
onClick={() => {
setShowTransferDialog(false);
setSelectedNewOwner('');
}}
disabled={transferring}
>
Cancel
</Button>
</div>
</div>
)}
</div>
)}
<LeagueOwnershipTransfer
settings={settings}
currentDriverId={currentDriverId}
onTransferOwnership={handleTransferOwnership}
/>
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
'use client';
import StandingsTable from '@/components/leagues/StandingsTable';
import LeagueChampionshipStats from '@/components/leagues/LeagueChampionshipStats';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import type { LeagueMembership, MembershipRole } from '@/lib/types';
@@ -86,54 +87,10 @@ export default function LeagueStandingsPage() {
);
}
const leader = standings[0];
const totalRaces = Math.max(...standings.map(s => s.races), 0);
return (
<div className="space-y-6">
{/* Championship Stats */}
{standings.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-yellow-500/10 flex items-center justify-center">
<span className="text-2xl">🏆</span>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Championship Leader</p>
<p className="font-bold text-white">{drivers.find(d => d.id === leader?.driverId)?.name || 'N/A'}</p>
<p className="text-sm text-yellow-400 font-medium">{leader?.points || 0} points</p>
</div>
</div>
</Card>
<Card>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-primary-blue/10 flex items-center justify-center">
<span className="text-2xl">🏁</span>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Races Completed</p>
<p className="text-2xl font-bold text-white">{totalRaces}</p>
<p className="text-sm text-gray-400">Season in progress</p>
</div>
</div>
</Card>
<Card>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-performance-green/10 flex items-center justify-center">
<span className="text-2xl">👥</span>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Active Drivers</p>
<p className="text-2xl font-bold text-white">{standings.length}</p>
<p className="text-sm text-gray-400">Competing for points</p>
</div>
</div>
</Card>
</div>
)}
<LeagueChampionshipStats standings={standings} drivers={drivers} />
<Card>
<h2 className="text-xl font-semibold text-white mb-4">Championship Standings</h2>

View File

@@ -3,6 +3,7 @@
import PenaltyFAB from '@/components/leagues/PenaltyFAB';
import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal';
import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import StewardingStats from '@/components/leagues/StewardingStats';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
@@ -183,29 +184,11 @@ export default function LeagueStewardingPage() {
{/* Stats summary */}
{!loading && stewardingData && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-warning-amber mb-1">
<Clock className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Pending Review</span>
</div>
<div className="text-2xl font-bold text-white">{stewardingData.totalPending}</div>
</div>
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-performance-green mb-1">
<CheckCircle className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Resolved</span>
</div>
<div className="text-2xl font-bold text-white">{stewardingData.totalResolved}</div>
</div>
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-red-400 mb-1">
<Gavel className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Penalties</span>
</div>
<div className="text-2xl font-bold text-white">{stewardingData.totalPenalties}</div>
</div>
</div>
<StewardingStats
totalPending={stewardingData.totalPending}
totalResolved={stewardingData.totalResolved}
totalPenalties={stewardingData.totalPenalties}
/>
)}
{/* Tab navigation */}

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import TransactionRow from '@/components/leagues/TransactionRow';
import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueWalletViewModel } from '@/lib/view-models/LeagueWalletViewModel';
import {
@@ -21,69 +22,6 @@ import {
Calendar
} from 'lucide-react';
function TransactionRow({ transaction }: { transaction: any }) {
const isIncoming = transaction.amount > 0;
const typeIcons = {
sponsorship: DollarSign,
membership: CreditCard,
withdrawal: ArrowUpRight,
prize: TrendingUp,
};
const TypeIcon = typeIcons[transaction.type];
const statusConfig = {
completed: { color: 'text-performance-green', bg: 'bg-performance-green/10', icon: CheckCircle },
pending: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', icon: Clock },
failed: { color: 'text-racing-red', bg: 'bg-racing-red/10', icon: XCircle },
};
const status = statusConfig[transaction.status];
const StatusIcon = status.icon;
return (
<div className="flex items-center justify-between p-4 border-b border-charcoal-outline last:border-b-0 hover:bg-iron-gray/30 transition-colors">
<div className="flex items-center gap-4">
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${isIncoming ? 'bg-performance-green/10' : 'bg-iron-gray/50'}`}>
{isIncoming ? (
<ArrowDownLeft className="w-5 h-5 text-performance-green" />
) : (
<ArrowUpRight className="w-5 h-5 text-gray-400" />
)}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-white">{transaction.description}</span>
<span className={`px-2 py-0.5 rounded text-xs ${status.bg} ${status.color}`}>
{transaction.status}
</span>
</div>
<div className="flex items-center gap-2 text-xs text-gray-500 mt-1">
<TypeIcon className="w-3 h-3" />
<span className="capitalize">{transaction.type}</span>
{transaction.reference && (
<>
<span></span>
<span>{transaction.reference}</span>
</>
)}
<span></span>
<span>{transaction.formattedDate}</span>
</div>
</div>
</div>
<div className="text-right">
<div className={`font-semibold ${isIncoming ? 'text-performance-green' : 'text-white'}`}>
{transaction.formattedAmount}
</div>
{transaction.fee > 0 && (
<div className="text-xs text-gray-500">
Fee: ${transaction.fee.toFixed(2)}
</div>
)}
</div>
</div>
);
}
export default function LeagueWalletPage() {
const params = useParams();

View File

@@ -3,6 +3,7 @@
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal';
import ImportResultsForm from '@/components/races/ImportResultsForm';
import RaceResultsHeader from '@/components/races/RaceResultsHeader';
import ResultsTable from '@/components/races/ResultsTable';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
@@ -132,50 +133,13 @@ export default function RaceResultsPage() {
</Button>
</div>
<div className="relative overflow-hidden rounded-2xl bg-gray-500/10 border border-gray-500/30 p-6 sm:p-8">
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full blur-3xl" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4">
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-performance-green/10 border border-performance-green/30">
<Trophy className="w-4 h-4 text-performance-green" />
<span className="text-sm font-semibold text-performance-green">
Final Results
</span>
</div>
{raceSOF && (
<span className="flex items-center gap-1.5 text-warning-amber text-sm">
<Zap className="w-4 h-4" />
SOF {raceSOF}
</span>
)}
</div>
<h1 className="text-2xl sm:text-3xl font-bold text-white mb-2">
{raceData?.race?.track ?? 'Race'} Results
</h1>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-gray-400">
{raceData?.race && (
<>
<span className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{new Date(raceData.race.scheduledAt).toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
})}
</span>
<span className="flex items-center gap-2">
<Users className="w-4 h-4" />
{raceData.stats.totalDrivers} drivers classified
</span>
</>
)}
{raceData?.league && <span className="text-primary-blue">{raceData.league.name}</span>}
</div>
</div>
</div>
<RaceResultsHeader
raceTrack={raceData?.race?.track}
raceScheduledAt={raceData?.race?.scheduledAt}
totalDrivers={raceData?.stats.totalDrivers}
leagueName={raceData?.league?.name}
raceSOF={raceSOF}
/>
{importSuccess && (
<div className="p-4 bg-performance-green/10 border border-performance-green/30 rounded-lg text-performance-green">

View File

@@ -1,6 +1,7 @@
'use client';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import RaceStewardingStats from '@/components/races/RaceStewardingStats';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
@@ -154,29 +155,11 @@ export default function RaceStewardingPage() {
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="rounded-lg bg-deep-graphite/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-warning-amber mb-1">
<Clock className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Pending</span>
</div>
<div className="text-2xl font-bold text-white">{stewardingData?.pendingCount ?? 0}</div>
</div>
<div className="rounded-lg bg-deep-graphite/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-performance-green mb-1">
<CheckCircle className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Resolved</span>
</div>
<div className="text-2xl font-bold text-white">{stewardingData?.resolvedCount ?? 0}</div>
</div>
<div className="rounded-lg bg-deep-graphite/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-red-400 mb-1">
<Gavel className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Penalties</span>
</div>
<div className="text-2xl font-bold text-white">{stewardingData?.penaltiesCount ?? 0}</div>
</div>
</div>
<RaceStewardingStats
pendingCount={stewardingData?.pendingCount ?? 0}
resolvedCount={stewardingData?.resolvedCount ?? 0}
penaltiesCount={stewardingData?.penaltiesCount ?? 0}
/>
</Card>
{/* Tab Navigation */}

View File

@@ -6,6 +6,10 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import StatusBadge from '@/components/ui/StatusBadge';
import InfoBanner from '@/components/ui/InfoBanner';
import MetricCard from '@/components/sponsors/MetricCard';
import SponsorshipCategoryCard from '@/components/sponsors/SponsorshipCategoryCard';
import ActivityItem from '@/components/sponsors/ActivityItem';
import RenewalAlert from '@/components/sponsors/RenewalAlert';
import {
BarChart3,
Eye,
@@ -35,146 +39,9 @@ import { SponsorService } from '@/lib/services/sponsors/SponsorService';
import { SponsorDashboardViewModel } from '@/lib/view-models/SponsorDashboardViewModel';
import { ServiceFactory } from '@/lib/services/ServiceFactory';
// Metric Card Component
function MetricCard({
title,
value,
change,
icon: Icon,
suffix = '',
prefix = '',
delay = 0,
}: {
title: string;
value: number | string;
change?: number;
icon: typeof Eye;
suffix?: string;
prefix?: string;
delay?: number;
}) {
const shouldReduceMotion = useReducedMotion();
const isPositive = change && change > 0;
const isNegative = change && change < 0;
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
>
<Card className="p-5 h-full">
<div className="flex items-start justify-between mb-3">
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-primary-blue/10">
<Icon className="w-5 h-5 text-primary-blue" />
</div>
{change !== undefined && (
<div className={`flex items-center gap-1 text-sm font-medium ${
isPositive ? 'text-performance-green' : isNegative ? 'text-racing-red' : 'text-gray-400'
}`}>
{isPositive ? <ArrowUpRight className="w-4 h-4" /> : isNegative ? <ArrowDownRight className="w-4 h-4" /> : null}
{Math.abs(change)}%
</div>
)}
</div>
<div className="text-2xl font-bold text-white mb-1">
{prefix}{typeof value === 'number' ? value.toLocaleString() : value}{suffix}
</div>
<div className="text-sm text-gray-400">{title}</div>
</Card>
</motion.div>
);
}
// Sponsorship Category Card
function SponsorshipCategoryCard({
icon: Icon,
title,
count,
impressions,
color,
href
}: {
icon: typeof Trophy;
title: string;
count: number;
impressions: number;
color: string;
href: string;
}) {
return (
<Link href={href}>
<Card className="p-4 hover:border-primary-blue/50 transition-all duration-300 cursor-pointer group">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg bg-iron-gray flex items-center justify-center group-hover:bg-primary-blue/10 transition-colors`}>
<Icon className={`w-5 h-5 ${color}`} />
</div>
<div>
<p className="text-white font-medium">{title}</p>
<p className="text-sm text-gray-500">{count} active</p>
</div>
</div>
<div className="text-right">
<p className="text-white font-semibold">{impressions.toLocaleString()}</p>
<p className="text-xs text-gray-500">impressions</p>
</div>
</div>
</Card>
</Link>
);
}
// Activity Item
function ActivityItem({ activity }: { activity: any }) {
return (
<div className="flex items-start gap-3 py-3 border-b border-charcoal-outline/50 last:border-b-0">
<div className={`w-2 h-2 rounded-full mt-2 ${activity.typeColor}`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-white truncate">{activity.message}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-gray-500">{activity.time}</span>
{activity.formattedImpressions && (
<>
<span className="text-xs text-gray-600"></span>
<span className="text-xs text-gray-400">{activity.formattedImpressions} views</span>
</>
)}
</div>
</div>
</div>
);
}
// Renewal Alert
function RenewalAlert({ renewal }: { renewal: any }) {
const typeIcons = {
league: Trophy,
team: Users,
driver: Car,
race: Flag,
platform: Megaphone,
};
const Icon = typeIcons[renewal.type] || Trophy;
return (
<div className="flex items-center justify-between p-3 rounded-lg bg-warning-amber/10 border border-warning-amber/30">
<div className="flex items-center gap-3">
<Icon className="w-4 h-4 text-warning-amber" />
<div>
<p className="text-sm text-white">{renewal.name}</p>
<p className="text-xs text-gray-400">Renews {renewal.formattedRenewDate}</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-white">{renewal.formattedPrice}</p>
<Button variant="secondary" className="text-xs mt-1 py-1 px-2 min-h-0">
Renew
</Button>
</div>
</div>
);
}
export default function SponsorDashboardPage() {
const shouldReduceMotion = useReducedMotion();

View File

@@ -12,6 +12,7 @@ import JoinTeamButton from '@/components/teams/JoinTeamButton';
import TeamAdmin from '@/components/teams/TeamAdmin';
import TeamRoster from '@/components/teams/TeamRoster';
import TeamStandings from '@/components/teams/TeamStandings';
import StatItem from '@/components/teams/StatItem';
import { useServices } from '@/lib/services/ServiceProvider';
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
@@ -319,12 +320,3 @@ export default function TeamDetailPage() {
</div>
);
}
function StatItem({ label, value, color }: { label: string; value: string; color: string }) {
return (
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">{label}</span>
<span className={`font-semibold ${color}`}>{value}</span>
</div>
);
}

View File

@@ -22,6 +22,7 @@ import {
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import TopThreePodium from '@/components/teams/TopThreePodium';
import { useAllTeams } from '@/hooks/useTeamService';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
@@ -109,146 +110,6 @@ const SORT_OPTIONS: { id: SortBy; label: string; icon: React.ElementType }[] = [
{ id: 'races', label: 'Races', icon: Hash },
];
// ============================================================================
// TOP THREE PODIUM COMPONENT
// ============================================================================
interface TopThreePodiumProps {
teams: TeamDisplayData[];
onTeamClick: (teamId: string) => void;
}
function TopThreePodium({ teams, onTeamClick }: TopThreePodiumProps) {
const top3 = teams.slice(0, 3) as [TeamDisplayData, TeamDisplayData, TeamDisplayData];
if (teams.length < 3) return null;
// Display order: 2nd, 1st, 3rd
const podiumOrder: [TeamDisplayData, TeamDisplayData, TeamDisplayData] = [
top3[1],
top3[0],
top3[2],
];
const podiumHeights = ['h-28', 'h-36', 'h-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 getGradient = (position: number) => {
switch (position) {
case 1:
return 'from-yellow-400/30 via-yellow-500/20 to-yellow-600/10';
case 2:
return 'from-gray-300/30 via-gray-400/20 to-gray-500/10';
case 3:
return 'from-amber-500/30 via-amber-600/20 to-amber-700/10';
default:
return 'from-gray-600/30 to-gray-700/10';
}
};
const getBorderColor = (position: number) => {
switch (position) {
case 1:
return 'border-yellow-400/50';
case 2:
return 'border-gray-300/50';
case 3:
return 'border-amber-600/50';
default:
return 'border-charcoal-outline';
}
};
return (
<div className="mb-10 p-8 rounded-2xl bg-gradient-to-br from-iron-gray/60 to-iron-gray/30 border border-charcoal-outline">
<div className="flex items-center justify-center gap-2 mb-8">
<Trophy className="w-6 h-6 text-yellow-400" />
<h2 className="text-xl font-bold text-white">Top 3 Teams</h2>
</div>
<div className="flex items-end justify-center gap-4 md:gap-8">
{podiumOrder.map((team, index) => {
const position = podiumPositions[index] ?? 0;
const levelConfig = SKILL_LEVELS.find((l) => l.id === team.performanceLevel);
const LevelIcon = levelConfig?.icon || Shield;
return (
<button
key={team.id}
type="button"
onClick={() => onTeamClick(team.id)}
className="flex flex-col items-center group"
>
{/* Team card */}
<div
className={`relative mb-4 p-4 rounded-xl bg-gradient-to-br ${getGradient(position ?? 0)} border ${getBorderColor(position ?? 0)} transition-all group-hover:scale-105 group-hover:shadow-lg`}
>
{/* Crown for 1st place */}
{position === 1 && (
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
<div className="relative">
<Crown className="w-8 h-8 text-yellow-400 animate-pulse" />
<div className="absolute inset-0 w-8 h-8 bg-yellow-400/30 blur-md rounded-full" />
</div>
</div>
)}
{/* Team icon */}
<div
className={`flex h-16 w-16 md:h-20 md:w-20 items-center justify-center rounded-xl ${levelConfig?.bgColor} border ${levelConfig?.borderColor} mb-3`}
>
<LevelIcon className={`w-8 h-8 md:w-10 md:h-10 ${levelConfig?.color}`} />
</div>
{/* Team name */}
<p className="text-white font-bold text-sm md:text-base text-center max-w-[120px] truncate group-hover:text-purple-400 transition-colors">
{team.name}
</p>
{/* Rating */}
<p className={`text-lg md:text-xl font-mono font-bold ${getPositionColor(position)} text-center`}>
{getSafeRating(team).toLocaleString()}
</p>
{/* Stats row */}
<div className="flex items-center justify-center gap-3 mt-2 text-xs text-gray-400">
<span className="flex items-center gap-1">
<Trophy className="w-3 h-3 text-performance-green" />
{getSafeTotalWins(team)}
</span>
<span className="flex items-center gap-1">
<Users className="w-3 h-3 text-purple-400" />
{team.memberCount}
</span>
</div>
</div>
{/* Podium stand */}
<div
className={`${podiumHeights[index]} w-20 md:w-28 rounded-t-lg bg-gradient-to-t ${getGradient(position)} border-t border-x ${getBorderColor(position)} flex items-start justify-center pt-3`}
>
<span className={`text-2xl md:text-3xl font-bold ${getPositionColor(position)}`}>
{position}
</span>
</div>
</button>
);
})}
</div>
</div>
);
}
// ============================================================================
// MAIN PAGE COMPONENT
@@ -455,7 +316,7 @@ export default function TeamLeaderboardPage() {
{/* Podium for Top 3 - only show when viewing by rating without filters */}
{sortBy === 'rating' && filterLevel === 'all' && !searchQuery && filteredAndSortedTeams.length >= 3 && (
<TopThreePodium teams={filteredAndSortedTeams} onTeamClick={handleTeamClick} />
<TopThreePodium teams={filteredAndSortedTeams} onClick={handleTeamClick} />
)}
{/* Stats Summary */}

View File

@@ -28,6 +28,10 @@ import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import CreateTeamForm from '@/components/teams/CreateTeamForm';
import WhyJoinTeamSection from '@/components/teams/WhyJoinTeamSection';
import SkillLevelSection from '@/components/teams/SkillLevelSection';
import FeaturedRecruiting from '@/components/teams/FeaturedRecruiting';
import TeamLeaderboardPreview from '@/components/teams/TeamLeaderboardPreview';
import { useAllTeams } from '@/hooks/useTeamService';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
@@ -88,341 +92,9 @@ const SKILL_LEVELS: {
},
];
// ============================================================================
// WHY JOIN A TEAM SECTION
// ============================================================================
function WhyJoinTeamSection() {
const benefits = [
{
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 (
<div className="mb-12">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Why Join a Team?</h2>
<p className="text-gray-400">Racing is better when you have teammates to share the journey</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{benefits.map((benefit) => (
<div
key={benefit.title}
className="p-5 rounded-xl bg-iron-gray/50 border border-charcoal-outline/50 hover:border-purple-500/30 transition-all duration-300"
>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10 border border-purple-500/20 mb-3">
<benefit.icon className="w-5 h-5 text-purple-400" />
</div>
<h3 className="text-white font-semibold mb-1">{benefit.title}</h3>
<p className="text-sm text-gray-500">{benefit.description}</p>
</div>
))}
</div>
</div>
);
}
// ============================================================================
// SKILL LEVEL SECTION COMPONENT
// ============================================================================
interface SkillLevelSectionProps {
level: typeof SKILL_LEVELS[0];
teams: TeamDisplayData[];
onTeamClick: (id: string) => void;
defaultExpanded?: boolean;
}
function SkillLevelSection({ level, teams, onTeamClick, defaultExpanded = false }: SkillLevelSectionProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const recruitingTeams = teams.filter((t) => t.isRecruiting);
const displayedTeams = isExpanded ? teams : teams.slice(0, 3);
const Icon = level.icon;
if (teams.length === 0) return null;
return (
<div className="mb-8">
{/* Section Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`flex h-11 w-11 items-center justify-center rounded-xl ${level.bgColor} border ${level.borderColor}`}>
<Icon className={`w-5 h-5 ${level.color}`} />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold text-white">{level.label}</h2>
<span className="px-2 py-0.5 rounded-full text-xs bg-charcoal-outline/50 text-gray-400">
{teams.length} {teams.length === 1 ? 'team' : 'teams'}
</span>
{recruitingTeams.length > 0 && (
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-performance-green/10 text-performance-green border border-performance-green/20">
<UserPlus className="w-3 h-3" />
{recruitingTeams.length} recruiting
</span>
)}
</div>
<p className="text-sm text-gray-500">{level.description}</p>
</div>
</div>
{teams.length > 3 && (
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-iron-gray/50 transition-all"
>
{isExpanded ? 'Show less' : `View all ${teams.length}`}
<ChevronRight className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
</button>
)}
</div>
{/* Teams Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{displayedTeams.map((team) => (
<TeamCard
key={team.id}
id={team.id}
name={team.name}
description={team.description ?? ''}
memberCount={team.memberCount}
rating={team.rating}
totalWins={team.totalWins}
totalRaces={team.totalRaces}
performanceLevel={team.performanceLevel}
isRecruiting={team.isRecruiting}
specialization={team.specialization}
region={team.region ?? ''}
languages={team.languages}
onClick={() => onTeamClick(team.id)}
/>
))}
</div>
</div>
);
}
// ============================================================================
// FEATURED RECRUITING TEAMS
// ============================================================================
interface FeaturedRecruitingProps {
teams: TeamDisplayData[];
onTeamClick: (id: string) => void;
}
function FeaturedRecruiting({ teams, onTeamClick }: FeaturedRecruitingProps) {
const recruitingTeams = teams.filter((t) => t.isRecruiting).slice(0, 4);
if (recruitingTeams.length === 0) return null;
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">
<UserPlus className="w-5 h-5 text-performance-green" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Looking for Drivers</h2>
<p className="text-xs text-gray-500">Teams actively recruiting new members</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{recruitingTeams.map((team) => {
const levelConfig = SKILL_LEVELS.find((l) => l.id === team.performanceLevel);
const LevelIcon = levelConfig?.icon || Shield;
return (
<button
key={team.id}
type="button"
onClick={() => onTeamClick(team.id)}
className="p-4 rounded-xl bg-iron-gray/60 border border-charcoal-outline hover:border-performance-green/40 transition-all duration-200 text-left group"
>
<div className="flex items-start justify-between mb-3">
<div className={`flex h-8 w-8 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
<LevelIcon className={`w-4 h-4 ${levelConfig?.color}`} />
</div>
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] bg-performance-green/10 text-performance-green border border-performance-green/20">
<div className="w-1.5 h-1.5 rounded-full bg-performance-green animate-pulse" />
Recruiting
</span>
</div>
<h3 className="text-white font-semibold mb-1 group-hover:text-performance-green transition-colors line-clamp-1">
{team.name}
</h3>
<p className="text-xs text-gray-500 line-clamp-2 mb-3">{team.description}</p>
<div className="flex items-center gap-3 text-xs text-gray-400">
<span className="flex items-center gap-1">
<Users className="w-3 h-3" />
{team.memberCount}
</span>
<span className="flex items-center gap-1">
<Trophy className="w-3 h-3" />
{team.totalWins} wins
</span>
</div>
</button>
);
})}
</div>
</div>
);
}
// ============================================================================
// TEAM LEADERBOARD PREVIEW COMPONENT (Top 5 + Link)
// ============================================================================
interface TeamLeaderboardPreviewProps {
topTeams: TeamDisplayData[];
onTeamClick: (id: string) => void;
}
function TeamLeaderboardPreview({ topTeams, onTeamClick }: TeamLeaderboardPreviewProps) {
const router = useRouter();
const getMedalColor = (position: number) => {
switch (position) {
case 0:
return 'text-yellow-400';
case 1:
return 'text-gray-300';
case 2:
return 'text-amber-600';
default:
return 'text-gray-500';
}
};
const getMedalBg = (position: number) => {
switch (position) {
case 0:
return 'bg-yellow-400/10 border-yellow-400/30';
case 1:
return 'bg-gray-300/10 border-gray-300/30';
case 2:
return 'bg-amber-600/10 border-amber-600/30';
default:
return 'bg-iron-gray/50 border-charcoal-outline';
}
};
if (topTeams.length === 0) return null;
return (
<div className="mb-12">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 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-xl font-bold text-white">Top Teams</h2>
<p className="text-sm text-gray-500">Highest rated racing teams</p>
</div>
</div>
<Button
variant="secondary"
onClick={() => router.push('/teams/leaderboard')}
className="flex items-center gap-2 text-sm"
>
View Full Leaderboard
<ChevronRight className="w-4 h-4" />
</Button>
</div>
{/* Compact Leaderboard */}
<div className="rounded-xl bg-iron-gray/30 border border-charcoal-outline overflow-hidden">
<div className="divide-y divide-charcoal-outline/50">
{topTeams.map((team, index) => {
const levelConfig = SKILL_LEVELS.find((l) => l.id === team.performanceLevel);
const LevelIcon = levelConfig?.icon || Shield;
return (
<button
key={team.id}
type="button"
onClick={() => onTeamClick(team.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(index)} ${getMedalColor(index)}`}
>
{index < 3 ? (
<Crown className="w-3.5 h-3.5" />
) : (
index + 1
)}
</div>
{/* Team Info */}
<div className={`flex h-9 w-9 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
<LevelIcon className={`w-4 h-4 ${levelConfig?.color}`} />
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-medium truncate group-hover:text-purple-400 transition-colors">
{team.name}
</p>
<div className="flex items-center gap-3 text-xs text-gray-500">
<span className="flex items-center gap-1">
<Users className="w-3 h-3" />
{team.memberCount}
</span>
<span className="flex items-center gap-1">
<Trophy className="w-3 h-3" />
{team.totalWins} wins
</span>
{team.isRecruiting && (
<span className="flex items-center gap-1 text-performance-green">
<div className="w-1.5 h-1.5 rounded-full bg-performance-green animate-pulse" />
Recruiting
</span>
)}
</div>
</div>
{/* Rating */}
<div className="text-right">
<p className="text-purple-400 font-mono font-semibold">
{team.rating?.toLocaleString()}
</p>
<p className="text-xs text-gray-500">Rating</p>
</div>
</button>
);
})}
</div>
</div>
</div>
);
}
// ============================================================================
// MAIN PAGE COMPONENT