page wrapper

This commit is contained in:
2026-01-07 12:40:52 +01:00
parent e589c30bf8
commit 0db80fa98d
128 changed files with 7386 additions and 8096 deletions

View File

@@ -0,0 +1,316 @@
'use client';
import {
Activity,
Award,
Calendar,
ChevronRight,
Clock,
Flag,
Medal,
Play,
Star,
Target,
Trophy,
UserPlus,
Users,
} from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { FeedItemRow } from '@/components/dashboard/FeedItemRow';
import { FriendItem } from '@/components/dashboard/FriendItem';
import { LeagueStandingItem } from '@/components/dashboard/LeagueStandingItem';
import { StatCard } from '@/components/dashboard/StatCard';
import { UpcomingRaceItem } from '@/components/dashboard/UpcomingRaceItem';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { getCountryFlag } from '@/lib/utilities/country';
import { getGreeting, timeUntil } from '@/lib/utilities/time';
import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
interface DashboardTemplateProps {
data: DashboardOverviewViewModel;
}
export function DashboardTemplate({ data }: DashboardTemplateProps) {
const currentDriver = data.currentDriver;
const nextRace = data.nextRace;
const upcomingRaces = data.upcomingRaces;
const leagueStandingsSummaries = data.leagueStandings;
const feedSummary = { items: data.feedItems };
const friends = data.friends;
const activeLeaguesCount = data.activeLeaguesCount;
const { totalRaces, wins, podiums, rating, globalRank, consistency } = currentDriver;
return (
<main className="min-h-screen bg-deep-graphite">
{/* Hero Section */}
<section className="relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 bg-gradient-to-br from-primary-blue/10 via-deep-graphite to-purple-600/5" />
<div className="absolute inset-0 opacity-5">
<div className="absolute inset-0" style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
}} />
</div>
<div className="relative max-w-7xl mx-auto px-6 py-10">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8">
{/* Welcome Message */}
<div className="flex items-start gap-5">
<div className="relative">
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-0.5 shadow-xl shadow-primary-blue/20">
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
<Image
src={currentDriver.avatarUrl}
alt={currentDriver.name}
width={80}
height={80}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-performance-green border-3 border-deep-graphite" />
</div>
<div>
<p className="text-gray-400 text-sm mb-1">{getGreeting()},</p>
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2">
{currentDriver.name}
<span className="ml-3 text-2xl">{getCountryFlag(currentDriver.country)}</span>
</h1>
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-primary-blue/10 border border-primary-blue/30">
<Star className="w-3.5 h-3.5 text-primary-blue" />
<span className="text-sm font-semibold text-primary-blue">{rating}</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-yellow-400/10 border border-yellow-400/30">
<Trophy className="w-3.5 h-3.5 text-yellow-400" />
<span className="text-sm font-semibold text-yellow-400">#{globalRank}</span>
</div>
<span className="text-xs text-gray-500">{totalRaces} races completed</span>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="flex flex-wrap gap-3">
<Link href="/leagues">
<Button variant="secondary" className="flex items-center gap-2">
<Flag className="w-4 h-4" />
Browse Leagues
</Button>
</Link>
<Link href="/profile">
<Button variant="primary" className="flex items-center gap-2">
<Activity className="w-4 h-4" />
View Profile
</Button>
</Link>
</div>
</div>
{/* Quick Stats Row */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
<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>
{/* Main Content */}
<section className="max-w-7xl mx-auto px-6 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Next Race Card */}
{nextRace && (
<Card className="relative overflow-hidden bg-gradient-to-br from-iron-gray to-iron-gray/80 border-primary-blue/30">
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-bl from-primary-blue/20 to-transparent rounded-bl-full" />
<div className="relative">
<div className="flex items-center gap-2 mb-4">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-primary-blue/20 border border-primary-blue/30">
<Play className="w-3.5 h-3.5 text-primary-blue" />
<span className="text-xs font-semibold text-primary-blue uppercase tracking-wider">Next Race</span>
</div>
{nextRace.isMyLeague && (
<span className="px-2 py-0.5 rounded-full bg-performance-green/20 text-performance-green text-xs font-medium">
Your League
</span>
)}
</div>
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-white mb-2">{nextRace.track}</h2>
<p className="text-gray-400 mb-3">{nextRace.car}</p>
<div className="flex flex-wrap items-center gap-4 text-sm">
<span className="flex items-center gap-1.5 text-gray-400">
<Calendar className="w-4 h-4" />
{nextRace.scheduledAt.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric',
})}
</span>
<span className="flex items-center gap-1.5 text-gray-400">
<Clock className="w-4 h-4" />
{nextRace.scheduledAt.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
</div>
<div className="flex flex-col items-end gap-3">
<div className="text-right">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Starts in</p>
<p className="text-3xl font-bold text-primary-blue font-mono">{timeUntil(nextRace.scheduledAt)}</p>
</div>
<Link href={`/races/${nextRace.id}`}>
<Button variant="primary" className="flex items-center gap-2">
View Details
<ChevronRight className="w-4 h-4" />
</Button>
</Link>
</div>
</div>
</div>
</Card>
)}
{/* League Standings Preview */}
{leagueStandingsSummaries.length > 0 && (
<Card>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<Award className="w-5 h-5 text-yellow-400" />
Your Championship Standings
</h2>
<Link href="/profile/leagues" className="text-sm text-primary-blue hover:underline flex items-center gap-1">
View all <ChevronRight className="w-4 h-4" />
</Link>
</div>
<div className="space-y-3">
{leagueStandingsSummaries.map((summary: any) => (
<LeagueStandingItem
key={summary.leagueId}
leagueId={summary.leagueId}
leagueName={summary.leagueName}
position={summary.position}
points={summary.points}
totalDrivers={summary.totalDrivers}
/>
))}
</div>
</Card>
)}
{/* Activity Feed */}
<Card>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<Activity className="w-5 h-5 text-cyan-400" />
Recent Activity
</h2>
</div>
{feedSummary.items.length > 0 ? (
<div className="space-y-4">
{feedSummary.items.slice(0, 5).map((item: any) => (
<FeedItemRow key={item.id} item={item} />
))}
</div>
) : (
<div className="text-center py-8">
<Activity className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400 mb-2">No activity yet</p>
<p className="text-sm text-gray-500">Join leagues and add friends to see activity here</p>
</div>
)}
</Card>
</div>
{/* Right Column - Sidebar */}
<div className="space-y-6">
{/* Upcoming Races */}
<Card>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Calendar className="w-5 h-5 text-red-400" />
Upcoming Races
</h3>
<Link href="/races" className="text-xs text-primary-blue hover:underline">
View all
</Link>
</div>
{upcomingRaces.length > 0 ? (
<div className="space-y-3">
{upcomingRaces.slice(0, 5).map((race: any) => (
<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>
)}
</Card>
{/* Friends */}
<Card>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Users className="w-5 h-5 text-purple-400" />
Friends
</h3>
<span className="text-xs text-gray-500">{friends.length} friends</span>
</div>
{friends.length > 0 ? (
<div className="space-y-2">
{friends.slice(0, 6).map((friend: any) => (
<FriendItem
key={friend.id}
id={friend.id}
name={friend.name}
avatarUrl={friend.avatarUrl}
country={friend.country}
/>
))}
{friends.length > 6 && (
<Link
href="/profile"
className="block text-center py-2 text-sm text-primary-blue hover:underline"
>
+{friends.length - 6} more
</Link>
)}
</div>
) : (
<div className="text-center py-6">
<UserPlus className="w-10 h-10 text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-400 mb-2">No friends yet</p>
<Link href="/drivers">
<Button variant="secondary" className="text-xs">
Find Drivers
</Button>
</Link>
</div>
)}
</Card>
</div>
</div>
</section>
</main>
);
}

View File

@@ -18,22 +18,19 @@ import { CategoryDistribution } from '@/components/drivers/CategoryDistribution'
import { LeaderboardPreview } from '@/components/drivers/LeaderboardPreview';
import { RecentActivity } from '@/components/drivers/RecentActivity';
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
interface DriversTemplateProps {
drivers: DriverLeaderboardItemViewModel[];
totalRaces: number;
totalWins: number;
activeCount: number;
isLoading?: boolean;
data: DriverLeaderboardViewModel | null;
}
export function DriversTemplate({
drivers,
totalRaces,
totalWins,
activeCount,
isLoading = false
}: DriversTemplateProps) {
export function DriversTemplate({ data }: DriversTemplateProps) {
const drivers = data?.drivers || [];
const totalRaces = data?.totalRaces || 0;
const totalWins = data?.totalWins || 0;
const activeCount = data?.activeCount || 0;
const isLoading = false;
const router = useRouter();
const [searchQuery, setSearchQuery] = useState('');

View File

@@ -0,0 +1,355 @@
import Image from 'next/image';
import Hero from '@/components/landing/Hero';
import AlternatingSection from '@/components/landing/AlternatingSection';
import FeatureGrid from '@/components/landing/FeatureGrid';
import DiscordCTA from '@/components/landing/DiscordCTA';
import FAQ from '@/components/landing/FAQ';
import Footer from '@/components/landing/Footer';
import CareerProgressionMockup from '@/components/mockups/CareerProgressionMockup';
import RaceHistoryMockup from '@/components/mockups/RaceHistoryMockup';
import CompanionAutomationMockup from '@/components/mockups/CompanionAutomationMockup';
import SimPlatformMockup from '@/components/mockups/SimPlatformMockup';
import MockupStack from '@/components/ui/MockupStack';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { getMediaUrl } from '@/lib/utilities/media';
export interface HomeTemplateData {
isAlpha: boolean;
upcomingRaces: any[];
topLeagues: any[];
teams: any[];
}
export interface HomeTemplateProps {
data: HomeTemplateData;
}
export default function HomeTemplate({ data }: HomeTemplateProps) {
return (
<main className="min-h-screen">
<Hero />
{/* Section 1: A Persistent Identity */}
<AlternatingSection
heading="A Persistent Identity"
backgroundVideo="/gameplay.mp4"
description={
<>
<p>
Your races, your seasons, your progress finally in one place.
</p>
<div className="space-y-3 mt-4">
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-4 border border-slate-700/40 hover:border-primary-blue/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(59,130,246,0.15)]">
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-primary-blue/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gradient-to-br from-primary-blue/20 to-blue-900/20 border border-primary-blue/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<svg className="w-5 h-5 text-primary-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-slate-200 leading-relaxed font-light">
Lifetime stats and season history across all your leagues
</span>
</div>
</div>
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-4 border border-slate-700/40 hover:border-primary-blue/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(59,130,246,0.15)]">
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-primary-blue/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gradient-to-br from-primary-blue/20 to-blue-900/20 border border-primary-blue/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<svg className="w-5 h-5 text-primary-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-slate-200 leading-relaxed font-light">
Track your performance, consistency, and team contributions
</span>
</div>
</div>
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-4 border border-slate-700/40 hover:border-primary-blue/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(59,130,246,0.15)]">
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-primary-blue/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gradient-to-br from-primary-blue/20 to-blue-900/20 border border-primary-blue/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<svg className="w-5 h-5 text-primary-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-slate-200 leading-relaxed font-light">
Your own rating that reflects real league competition
</span>
</div>
</div>
</div>
<p className="mt-4">
iRacing gives you physics. GridPilot gives you a career.
</p>
</>
}
mockup={<CareerProgressionMockup />}
layout="text-left"
/>
<FeatureGrid />
{/* Section 2: Results That Actually Stay */}
<AlternatingSection
heading="Results That Actually Stay"
backgroundImage="/images/ff1600.jpeg"
description={
<>
<p className="text-sm md:text-base leading-relaxed">
Every race you run stays with you.
</p>
<div className="space-y-3 mt-4 md:mt-6">
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-3.5 md:p-4 border border-slate-700/40 hover:border-red-600/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(220,38,38,0.15)]">
<div className="absolute top-0 right-0 w-12 h-12 bg-gradient-to-bl from-red-600/10 to-transparent rounded-bl-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-2.5 md:gap-3">
<div className="flex-shrink-0 w-8 h-8 md:w-9 md:h-9 rounded-lg bg-gradient-to-br from-red-600/20 to-red-900/20 border border-red-600/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<svg className="w-4 h-4 md:w-5 md:h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
</div>
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">
Your stats, your team, your story all connected
</span>
</div>
</div>
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-3.5 md:p-4 border border-slate-700/40 hover:border-red-600/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(220,38,38,0.15)]">
<div className="absolute top-0 right-0 w-12 h-12 bg-gradient-to-bl from-red-600/10 to-transparent rounded-bl-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-2.5 md:gap-3">
<div className="flex-shrink-0 w-8 h-8 md:w-9 md:h-9 rounded-lg bg-gradient-to-br from-red-600/20 to-red-900/20 border border-red-600/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<svg className="w-4 h-4 md:w-5 md:h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">
One race result updates your profile, team points, rating, and season history
</span>
</div>
</div>
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-3.5 md:p-4 border border-slate-700/40 hover:border-red-600/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(220,38,38,0.15)]">
<div className="absolute top-0 right-0 w-12 h-12 bg-gradient-to-bl from-red-600/10 to-transparent rounded-bl-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-2.5 md:gap-3">
<div className="flex-shrink-0 w-8 h-8 md:w-9 md:h-9 rounded-lg bg-gradient-to-br from-red-600/20 to-red-900/20 border border-red-600/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<svg className="w-4 h-4 md:w-5 md:h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">
No more fragmented data across spreadsheets and forums
</span>
</div>
</div>
</div>
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
Your racing career, finally in one place.
</p>
</>
}
mockup={<MockupStack index={1}><RaceHistoryMockup /></MockupStack>}
layout="text-right"
/>
{/* Section 3: Automatic Session Creation */}
<AlternatingSection
heading="Automatic Session Creation"
description={
<>
<p className="text-sm md:text-base leading-relaxed">
Setting up league races used to mean clicking through iRacing's wizard 20 times.
</p>
<div className="space-y-3 mt-4 md:mt-6">
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-br from-slate-900/70 to-slate-800/50 p-3.5 md:p-4 border border-slate-700/50 hover:border-primary-blue/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(59,130,246,0.2)]">
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-primary-blue/10 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-2.5 md:gap-3 relative">
<div className="flex-shrink-0 w-9 h-9 md:w-10 md:h-10 rounded-xl bg-gradient-to-br from-primary-blue/25 to-blue-900/25 border border-primary-blue/40 flex items-center justify-center shadow-lg group-hover:shadow-primary-blue/20 group-hover:scale-110 transition-all">
<span className="text-primary-blue font-bold text-sm">1</span>
</div>
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">
Our companion app syncs with your league schedule
</span>
</div>
</div>
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-br from-slate-900/70 to-slate-800/50 p-3.5 md:p-4 border border-slate-700/50 hover:border-primary-blue/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(59,130,246,0.2)]">
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-primary-blue/10 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-2.5 md:gap-3 relative">
<div className="flex-shrink-0 w-9 h-9 md:w-10 md:h-10 rounded-xl bg-gradient-to-br from-primary-blue/25 to-blue-900/25 border border-primary-blue/40 flex items-center justify-center shadow-lg group-hover:shadow-primary-blue/20 group-hover:scale-110 transition-all">
<span className="text-primary-blue font-bold text-sm">2</span>
</div>
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">
When it's race time, it creates the iRacing session automatically
</span>
</div>
</div>
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-br from-slate-900/70 to-slate-800/50 p-3.5 md:p-4 border border-slate-700/50 hover:border-primary-blue/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(59,130,246,0.2)]">
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-primary-blue/10 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start gap-2.5 md:gap-3 relative">
<div className="flex-shrink-0 w-9 h-9 md:w-10 md:h-10 rounded-xl bg-gradient-to-br from-primary-blue/25 to-blue-900/25 border border-primary-blue/40 flex items-center justify-center shadow-lg group-hover:shadow-primary-blue/20 group-hover:scale-110 transition-all">
<span className="text-primary-blue font-bold text-sm">3</span>
</div>
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">
No clicking through wizards. No manual setup
</span>
</div>
</div>
</div>
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
Automation instead of repetition.
</p>
</>
}
mockup={<CompanionAutomationMockup />}
layout="text-left"
/>
{/* Section 4: Game-Agnostic Platform */}
<AlternatingSection
heading="Built for iRacing. Ready for the future."
backgroundImage="/images/lmp3.jpeg"
description={
<>
<p className="text-sm md:text-base leading-relaxed">
Right now, we're focused on making iRacing league racing better.
</p>
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
But sims come and go. Your leagues, your teams, your rating — those stay.
</p>
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
GridPilot is built to outlast any single platform.
</p>
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
When the next sim arrives, your competitive identity moves with you.
</p>
</>
}
mockup={<SimPlatformMockup />}
layout="text-right"
/>
{/* Alpha-only discovery section */}
{data.isAlpha && (
<section className="max-w-7xl mx-auto mt-20 mb-20 px-6">
<div className="flex items-baseline justify-between mb-8">
<div>
<h2 className="text-2xl font-semibold text-white">Discover the grid</h2>
<p className="text-sm text-gray-400">
Explore leagues, teams, and races that make up the GridPilot ecosystem.
</p>
</div>
</div>
<div className="grid gap-8 lg:grid-cols-3">
{/* Top leagues */}
<Card className="bg-iron-gray/80">
<div className="flex items-baseline justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Featured leagues</h3>
<Button
as="a"
href="/leagues"
variant="secondary"
className="text-[11px] px-3 py-1.5"
>
View all
</Button>
</div>
<ul className="space-y-3 text-sm">
{data.topLeagues.slice(0, 4).map((league: any) => (
<li key={league.id} className="flex items-start gap-3">
<div className="w-10 h-10 rounded-md bg-primary-blue/15 border border-primary-blue/30 flex items-center justify-center text-xs font-semibold text-primary-blue">
{league.name
.split(' ')
.map((word: string) => word[0])
.join('')
.slice(0, 3)
.toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-white truncate">{league.name}</p>
<p className="text-xs text-gray-400 line-clamp-2">
{league.description}
</p>
</div>
</li>
))}
</ul>
</Card>
{/* Teams */}
<Card className="bg-iron-gray/80">
<div className="flex items-baseline justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Teams on the grid</h3>
<Button
as="a"
href="/teams"
variant="secondary"
className="text-[11px] px-3 py-1.5"
>
Browse teams
</Button>
</div>
<ul className="space-y-3 text-sm">
{data.teams.slice(0, 4).map(team => (
<li key={team.id} className="flex items-start gap-3">
<div className="w-10 h-10 rounded-md bg-charcoal-outline flex items-center justify-center overflow-hidden border border-charcoal-outline">
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={40}
height={40}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-white truncate">{team.name}</p>
<p className="text-xs text-gray-400 line-clamp-2">
{team.description}
</p>
</div>
</li>
))}
</ul>
</Card>
{/* Upcoming races */}
<Card className="bg-iron-gray/80">
<div className="flex items-baseline justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Upcoming races</h3>
<Button
as="a"
href="/races"
variant="secondary"
className="text-[11px] px-3 py-1.5"
>
View schedule
</Button>
</div>
{data.upcomingRaces.length === 0 ? (
<p className="text-xs text-gray-400">
No races scheduled in this demo snapshot.
</p>
) : (
<ul className="space-y-3 text-sm">
{data.upcomingRaces.map(race => (
<li key={race.id} className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<p className="text-white truncate">{race.track}</p>
<p className="text-xs text-gray-400 truncate">{race.car}</p>
</div>
<div className="text-right text-xs text-gray-500 whitespace-nowrap">
{race.formattedDate}
</div>
</li>
))}
</ul>
)}
</Card>
</div>
</section>
)}
<DiscordCTA />
<FAQ />
<Footer />
</main>
);
}

View File

@@ -0,0 +1,234 @@
'use client';
import { useMemo, useState } from 'react';
import type { LeagueAdminScheduleViewModel } from '@/lib/view-models/LeagueAdminScheduleViewModel';
import type { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
import Card from '@/components/ui/Card';
// ============================================================================
// TYPES
// ============================================================================
interface LeagueAdminScheduleTemplateProps {
data: {
schedule: LeagueAdminScheduleViewModel;
seasons: LeagueSeasonSummaryViewModel[];
seasonId: string;
};
onSeasonChange: (seasonId: string) => void;
onPublishToggle: () => void;
onAddOrSave: () => void;
onEdit: (raceId: string) => void;
onDelete: (raceId: string) => void;
onCancelEdit: () => void;
// Form state
track: string;
car: string;
scheduledAtIso: string;
editingRaceId: string | null;
// Mutation states
isPublishing: boolean;
isSaving: boolean;
isDeleting: string | null;
// Form setters
setTrack: (value: string) => void;
setCar: (value: string) => void;
setScheduledAtIso: (value: string) => void;
}
// ============================================================================
// MAIN TEMPLATE COMPONENT
// ============================================================================
export function LeagueAdminScheduleTemplate({
data,
onSeasonChange,
onPublishToggle,
onAddOrSave,
onEdit,
onDelete,
onCancelEdit,
track,
car,
scheduledAtIso,
editingRaceId,
isPublishing,
isSaving,
isDeleting,
setTrack,
setCar,
setScheduledAtIso,
}: LeagueAdminScheduleTemplateProps) {
const { schedule, seasons, seasonId } = data;
const isEditing = editingRaceId !== null;
const publishedLabel = schedule.published ? 'Published' : 'Unpublished';
const selectedSeasonLabel = useMemo(() => {
const selected = seasons.find((s) => s.seasonId === seasonId);
return selected?.name ?? seasonId;
}, [seasons, seasonId]);
return (
<div className="space-y-6">
<Card>
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-white">Schedule Admin</h1>
<p className="text-sm text-gray-400">Create, edit, and publish season races.</p>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm text-gray-300" htmlFor="seasonId">
Season
</label>
{seasons.length > 0 ? (
<select
id="seasonId"
value={seasonId}
onChange={(e) => onSeasonChange(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
>
{seasons.map((s) => (
<option key={s.seasonId} value={s.seasonId}>
{s.name}
</option>
))}
</select>
) : (
<input
id="seasonId"
value={seasonId}
onChange={(e) => onSeasonChange(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
placeholder="season-id"
/>
)}
<p className="text-xs text-gray-500">Selected: {selectedSeasonLabel}</p>
</div>
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-gray-300">
Status: <span className="font-medium text-white">{publishedLabel}</span>
</p>
<button
type="button"
onClick={onPublishToggle}
disabled={!schedule || isPublishing}
className="px-3 py-1.5 rounded bg-primary-blue text-white disabled:opacity-50"
>
{isPublishing ? 'Processing...' : (schedule?.published ? 'Unpublish' : 'Publish')}
</button>
</div>
<div className="border-t border-charcoal-outline pt-4 space-y-3">
<h2 className="text-lg font-semibold text-white">{isEditing ? 'Edit race' : 'Add race'}</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="track" className="text-sm text-gray-300">
Track
</label>
<input
id="track"
value={track}
onChange={(e) => setTrack(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="car" className="text-sm text-gray-300">
Car
</label>
<input
id="car"
value={car}
onChange={(e) => setCar(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="scheduledAtIso" className="text-sm text-gray-300">
Scheduled At (ISO)
</label>
<input
id="scheduledAtIso"
value={scheduledAtIso}
onChange={(e) => setScheduledAtIso(e.target.value)}
className="bg-iron-gray text-white px-3 py-2 rounded"
placeholder="2025-01-01T12:00:00.000Z"
/>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onAddOrSave}
disabled={isSaving}
className="px-3 py-1.5 rounded bg-primary-blue text-white"
>
{isSaving ? 'Processing...' : (isEditing ? 'Save' : 'Add race')}
</button>
{isEditing && (
<button
type="button"
onClick={onCancelEdit}
className="px-3 py-1.5 rounded bg-iron-gray text-gray-200"
>
Cancel
</button>
)}
</div>
</div>
<div className="border-t border-charcoal-outline pt-4 space-y-3">
<h2 className="text-lg font-semibold text-white">Races</h2>
{schedule?.races.length ? (
<div className="space-y-2">
{schedule.races.map((race) => (
<div
key={race.id}
className="flex items-center justify-between gap-3 bg-deep-graphite border border-charcoal-outline rounded p-3"
>
<div className="min-w-0">
<p className="text-white font-medium truncate">{race.name}</p>
<p className="text-xs text-gray-400 truncate">{race.scheduledAt.toISOString()}</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => onEdit(race.id)}
className="px-3 py-1.5 rounded bg-iron-gray text-gray-200"
>
Edit
</button>
<button
type="button"
onClick={() => onDelete(race.id)}
disabled={isDeleting === race.id}
className="px-3 py-1.5 rounded bg-iron-gray text-gray-200"
>
{isDeleting === race.id ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
))}
</div>
) : (
<div className="py-4 text-sm text-gray-500">No races yet.</div>
)}
</div>
</div>
</Card>
</div>
);
}

View File

@@ -1,6 +1,14 @@
'use client';
import LeagueSchedule from '@/components/leagues/LeagueSchedule';
import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import type { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
import { StateContainer } from '@/components/shared/state/StateContainer';
import { EmptyState } from '@/components/shared/state/EmptyState';
import { Calendar } from 'lucide-react';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useRegisterForRace } from '@/hooks/race/useRegisterForRace';
import { useWithdrawFromRace } from '@/hooks/race/useWithdrawFromRace';
import Card from '@/components/ui/Card';
// ============================================================================
@@ -8,8 +16,8 @@ import Card from '@/components/ui/Card';
// ============================================================================
interface LeagueScheduleTemplateProps {
data: LeagueScheduleViewModel;
leagueId: string;
loading?: boolean;
}
// ============================================================================
@@ -17,22 +25,224 @@ interface LeagueScheduleTemplateProps {
// ============================================================================
export function LeagueScheduleTemplate({
data,
leagueId,
loading = false,
}: LeagueScheduleTemplateProps) {
if (loading) {
return (
<div className="text-center text-gray-400">
Loading schedule...
</div>
);
}
const router = useRouter();
const [filter, setFilter] = useState<'all' | 'upcoming' | 'past'>('upcoming');
const currentDriverId = useEffectiveDriverId();
const registerMutation = useRegisterForRace();
const withdrawMutation = useWithdrawFromRace();
const races = useMemo(() => {
return data?.races ?? [];
}, [data]);
const upcomingRaces = races.filter((race) => race.isUpcoming);
const pastRaces = races.filter((race) => race.isPast);
const getDisplayRaces = () => {
switch (filter) {
case 'upcoming':
return upcomingRaces;
case 'past':
return [...pastRaces].reverse();
case 'all':
return [...upcomingRaces, ...[...pastRaces].reverse()];
default:
return races;
}
};
const displayRaces = getDisplayRaces();
const handleRegister = async (race: LeagueScheduleRaceViewModel, e: React.MouseEvent) => {
e.stopPropagation();
const confirmed = window.confirm(`Register for ${race.track ?? race.name}?`);
if (!confirmed) return;
if (!currentDriverId) {
alert('You must be logged in to register for races');
return;
}
try {
await registerMutation.mutateAsync({ raceId: race.id, leagueId, driverId: currentDriverId });
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to register');
}
};
const handleWithdraw = async (race: LeagueScheduleRaceViewModel, e: React.MouseEvent) => {
e.stopPropagation();
const confirmed = window.confirm('Withdraw from this race?');
if (!confirmed) return;
if (!currentDriverId) {
alert('You must be logged in to withdraw from races');
return;
}
try {
await withdrawMutation.mutateAsync({ raceId: race.id, driverId: currentDriverId });
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to withdraw');
}
};
return (
<div className="space-y-6">
<Card>
<h2 className="text-xl font-semibold text-white mb-4">Schedule</h2>
<LeagueSchedule leagueId={leagueId} />
{/* Filter Controls */}
<div className="mb-4 flex items-center justify-between">
<p className="text-sm text-gray-400">
{displayRaces.length} {displayRaces.length === 1 ? 'race' : 'races'}
</p>
<div className="flex gap-2">
<button
onClick={() => setFilter('upcoming')}
className={`px-3 py-1 text-sm font-medium rounded transition-colors ${
filter === 'upcoming'
? 'bg-primary-blue text-white'
: 'bg-iron-gray text-gray-400 hover:text-white'
}`}
>
Upcoming ({upcomingRaces.length})
</button>
<button
onClick={() => setFilter('past')}
className={`px-3 py-1 text-sm font-medium rounded transition-colors ${
filter === 'past'
? 'bg-primary-blue text-white'
: 'bg-iron-gray text-gray-400 hover:text-white'
}`}
>
Past ({pastRaces.length})
</button>
<button
onClick={() => setFilter('all')}
className={`px-3 py-1 text-sm font-medium rounded transition-colors ${
filter === 'all'
? 'bg-primary-blue text-white'
: 'bg-iron-gray text-gray-400 hover:text-white'
}`}
>
All ({races.length})
</button>
</div>
</div>
{/* Race List */}
{displayRaces.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<p className="mb-2">No {filter} races</p>
{filter === 'upcoming' && (
<p className="text-sm text-gray-500">Schedule your first race to get started</p>
)}
</div>
) : (
<div className="space-y-3">
{displayRaces.map((race) => {
const isPast = race.isPast;
const isUpcoming = race.isUpcoming;
const isRegistered = Boolean(race.isRegistered);
const trackLabel = race.track ?? race.name;
const carLabel = race.car ?? '—';
const sessionTypeLabel = (race.sessionType ?? 'race').toLowerCase();
const isProcessing =
registerMutation.isPending || withdrawMutation.isPending;
return (
<div
key={race.id}
className={`p-4 rounded-lg border transition-all duration-200 cursor-pointer hover:scale-[1.02] ${
isPast
? 'bg-iron-gray/50 border-charcoal-outline/50 opacity-75'
: 'bg-deep-graphite border-charcoal-outline hover:border-primary-blue'
}`}
onClick={() => router.push(`/races/${race.id}`)}
>
<div className="flex items-center justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<h3 className="text-white font-medium">{trackLabel}</h3>
{isUpcoming && !isRegistered && (
<span className="px-2 py-0.5 text-xs font-medium bg-primary-blue/10 text-primary-blue rounded border border-primary-blue/30">
Upcoming
</span>
)}
{isUpcoming && isRegistered && (
<span className="px-2 py-0.5 text-xs font-medium bg-green-500/10 text-green-400 rounded border border-green-500/30">
Registered
</span>
)}
{isPast && (
<span className="px-2 py-0.5 text-xs font-medium bg-gray-700/50 text-gray-400 rounded border border-gray-600/50">
Completed
</span>
)}
</div>
<p className="text-sm text-gray-400">{carLabel}</p>
<div className="flex items-center gap-3 mt-2">
<p className="text-xs text-gray-500 uppercase">{sessionTypeLabel}</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<p className="text-white font-medium">
{race.scheduledAt.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</p>
<p className="text-sm text-gray-400">
{race.scheduledAt.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</p>
{isPast && race.status === 'completed' && (
<p className="text-xs text-primary-blue mt-1">View Results </p>
)}
</div>
{/* Registration Actions */}
{isUpcoming && (
<div onClick={(e) => e.stopPropagation()}>
{!isRegistered ? (
<button
onClick={(e) => handleRegister(race, e)}
disabled={isProcessing}
className="px-3 py-1.5 text-sm font-medium bg-primary-blue hover:bg-primary-blue/80 text-white rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
>
{registerMutation.isPending ? 'Registering...' : 'Register'}
</button>
) : (
<button
onClick={(e) => handleWithdraw(race, e)}
disabled={isProcessing}
className="px-3 py-1.5 text-sm font-medium bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
>
{withdrawMutation.isPending ? 'Withdrawing...' : 'Withdraw'}
</button>
)}
</div>
)}
</div>
</div>
</div>
);
})}
</div>
)}
</Card>
</div>
);

View File

@@ -58,18 +58,16 @@ interface LeagueSliderProps {
icon: React.ElementType;
description: string;
leagues: LeagueSummaryViewModel[];
onLeagueClick: (id: string) => void;
autoScroll?: boolean;
iconColor?: string;
scrollSpeedMultiplier?: number;
scrollDirection?: 'left' | 'right';
}
import Link from 'next/link';
interface LeaguesTemplateProps {
leagues: LeagueSummaryViewModel[];
loading?: boolean;
onLeagueClick: (id: string) => void;
onCreateLeagueClick: () => void;
data: LeagueSummaryViewModel[];
}
// ============================================================================
@@ -183,7 +181,6 @@ function LeagueSlider({
icon: Icon,
description,
leagues,
onLeagueClick,
autoScroll = true,
iconColor = 'text-primary-blue',
scrollSpeedMultiplier = 1,
@@ -372,7 +369,9 @@ function LeagueSlider({
`}</style>
{leagues.map((league) => (
<div key={league.id} className="flex-shrink-0 w-[320px] h-full">
<LeagueCard league={league} onClick={() => onLeagueClick(league.id)} />
<Link href={`/leagues/${league.id}`} className="block h-full">
<LeagueCard league={league} />
</Link>
</div>
))}
</div>
@@ -386,17 +385,14 @@ function LeagueSlider({
// ============================================================================
export function LeaguesTemplate({
leagues,
loading = false,
onLeagueClick,
onCreateLeagueClick,
data,
}: LeaguesTemplateProps) {
const [searchQuery, setSearchQuery] = useState('');
const [activeCategory, setActiveCategory] = useState<CategoryId>('all');
const [showFilters, setShowFilters] = useState(false);
// Filter by search query
const searchFilteredLeagues = leagues.filter((league) => {
const searchFilteredLeagues = data.filter((league: LeagueSummaryViewModel) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
@@ -416,7 +412,7 @@ export function LeaguesTemplate({
const leaguesByCategory = CATEGORIES.reduce(
(acc, category) => {
// First try to use the dedicated category field, fall back to scoring-based filtering
acc[category.id] = searchFilteredLeagues.filter((league) => {
acc[category.id] = searchFilteredLeagues.filter((league: LeagueSummaryViewModel) => {
// If league has a category field, use it directly
if (league.category) {
return league.category === category.id;
@@ -440,19 +436,6 @@ export function LeaguesTemplate({
{ id: 'sprint', speed: 1.2, direction: 'right' },
];
if (loading) {
return (
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center justify-center min-h-[400px]">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 border-2 border-primary-blue border-t-transparent rounded-full animate-spin" />
<p className="text-gray-400">Loading leagues...</p>
</div>
</div>
</div>
);
}
return (
<div className="max-w-7xl mx-auto px-4 pb-12">
{/* Hero Section */}
@@ -480,7 +463,7 @@ export function LeaguesTemplate({
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-performance-green animate-pulse" />
<span className="text-sm text-gray-400">
<span className="text-white font-semibold">{leagues.length}</span> active leagues
<span className="text-white font-semibold">{data.length}</span> active leagues
</span>
</div>
<div className="flex items-center gap-2">
@@ -500,14 +483,10 @@ export function LeaguesTemplate({
{/* CTA */}
<div className="flex flex-col gap-4">
<Button
variant="primary"
onClick={onCreateLeagueClick}
className="flex items-center gap-2 px-6 py-3"
>
<Link href="/leagues/create" className="flex items-center gap-2 px-6 py-3 bg-primary-blue text-white rounded-lg hover:bg-blue-600 transition-colors">
<Plus className="w-5 h-5" />
<span>Create League</span>
</Button>
</Link>
<p className="text-xs text-gray-500 text-center">Set up your own racing series</p>
</div>
</div>
@@ -574,7 +553,7 @@ export function LeaguesTemplate({
</div>
{/* Content */}
{leagues.length === 0 ? (
{data.length === 0 ? (
/* Empty State */
<Card className="text-center py-16">
<div className="max-w-md mx-auto">
@@ -587,14 +566,10 @@ export function LeaguesTemplate({
<p className="text-gray-400 mb-8">
Be the first to create a racing series. Start your own league and invite drivers to compete for glory.
</p>
<Button
variant="primary"
onClick={onCreateLeagueClick}
className="flex items-center gap-2 mx-auto"
>
<Link href="/leagues/create" className="inline-flex items-center gap-2 px-6 py-3 bg-primary-blue text-white rounded-lg hover:bg-blue-600 transition-colors">
<Sparkles className="w-4 h-4" />
Create Your First League
</Button>
</Link>
</div>
</Card>
) : activeCategory === 'all' && !searchQuery ? (
@@ -613,7 +588,6 @@ export function LeaguesTemplate({
icon={category.icon}
description={category.description}
leagues={leaguesByCategory[category.id]}
onLeagueClick={onLeagueClick}
autoScroll={true}
iconColor={category.color || 'text-primary-blue'}
scrollSpeedMultiplier={speed}
@@ -640,7 +614,9 @@ export function LeaguesTemplate({
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{categoryFilteredLeagues.map((league) => (
<LeagueCard key={league.id} league={league} onClick={() => onLeagueClick(league.id)} />
<Link key={league.id} href={`/leagues/${league.id}`} className="block h-full">
<LeagueCard league={league} />
</Link>
))}
</div>
</>

View File

@@ -0,0 +1,578 @@
'use client';
import { useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import Link from 'next/link';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { siteConfig } from '@/lib/siteConfig';
import {
Trophy,
Users,
Calendar,
Eye,
TrendingUp,
Download,
Image as ImageIcon,
ExternalLink,
ChevronRight,
Star,
Clock,
CheckCircle2,
Flag,
Car,
BarChart3,
ArrowUpRight,
Megaphone,
CreditCard,
FileText
} from 'lucide-react';
interface SponsorLeagueDetailData {
league: {
id: string;
name: string;
game: string;
season: string;
description: string;
tier: 'premium' | 'standard' | 'starter';
rating: number;
drivers: number;
races: number;
completedRaces: number;
racesLeft: number;
engagement: number;
totalImpressions: number;
formattedTotalImpressions: string;
projectedTotal: number;
formattedProjectedTotal: string;
mainSponsorCpm: number;
formattedMainSponsorCpm: string;
avgViewsPerRace: number;
formattedAvgViewsPerRace: string;
nextRace?: {
name: string;
date: string;
};
sponsorSlots: {
main: {
available: boolean;
price: number;
benefits: string[];
};
secondary: {
available: number;
total: number;
price: number;
benefits: string[];
};
};
tierConfig: {
bgColor: string;
color: string;
border: string;
};
};
drivers: Array<{
id: string;
position: number;
name: string;
team: string;
country: string;
races: number;
impressions: number;
formattedImpressions: string;
}>;
races: Array<{
id: string;
name: string;
date: string;
formattedDate: string;
status: 'completed' | 'upcoming';
views: number;
}>;
}
interface SponsorLeagueDetailTemplateProps {
data: SponsorLeagueDetailData;
}
type TabType = 'overview' | 'drivers' | 'races' | 'sponsor';
export function SponsorLeagueDetailTemplate({ data }: SponsorLeagueDetailTemplateProps) {
const shouldReduceMotion = useReducedMotion();
const [activeTab, setActiveTab] = useState<TabType>('overview');
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');
const league = data.league;
const config = league.tierConfig;
return (
<div className="max-w-7xl mx-auto py-8 px-4">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-6">
<Link href="/sponsor/dashboard" className="hover:text-white transition-colors">Dashboard</Link>
<ChevronRight className="w-4 h-4" />
<Link href="/sponsor/leagues" className="hover:text-white transition-colors">Leagues</Link>
<ChevronRight className="w-4 h-4" />
<span className="text-white">{league.name}</span>
</div>
{/* Header */}
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-6 mb-8">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className={`px-3 py-1 rounded-full text-sm font-medium capitalize ${config.bgColor} ${config.color} border ${config.border}`}>
{league.tier}
</span>
<span className="px-3 py-1 rounded-full text-sm font-medium bg-performance-green/10 text-performance-green">
Active Season
</span>
<div className="flex items-center gap-1 px-2 py-1 rounded bg-iron-gray/50">
<Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />
<span className="text-sm font-medium text-white">{league.rating}</span>
</div>
</div>
<h1 className="text-3xl font-bold text-white mb-2">{league.name}</h1>
<p className="text-gray-400 mb-4">{league.game} {league.season} {league.completedRaces}/{league.races} races completed</p>
<p className="text-gray-400 max-w-2xl">{league.description}</p>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<Link href={`/leagues/${league.id}`}>
<Button variant="secondary">
<ExternalLink className="w-4 h-4 mr-2" />
View League
</Button>
</Link>
{(league.sponsorSlots.main.available || league.sponsorSlots.secondary.available > 0) && (
<Button variant="primary" onClick={() => setActiveTab('sponsor')}>
<Megaphone className="w-4 h-4 mr-2" />
Become a Sponsor
</Button>
)}
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/10">
<Eye className="w-5 h-5 text-primary-blue" />
</div>
<div>
<div className="text-xl font-bold text-white">{league.formattedTotalImpressions}</div>
<div className="text-xs text-gray-400">Total Views</div>
</div>
</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-performance-green/10">
<TrendingUp className="w-5 h-5 text-performance-green" />
</div>
<div>
<div className="text-xl font-bold text-white">{league.formattedAvgViewsPerRace}</div>
<div className="text-xs text-gray-400">Avg/Race</div>
</div>
</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10">
<Users className="w-5 h-5 text-purple-400" />
</div>
<div>
<div className="text-xl font-bold text-white">{league.drivers}</div>
<div className="text-xs text-gray-400">Drivers</div>
</div>
</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-warning-amber/10">
<BarChart3 className="w-5 h-5 text-warning-amber" />
</div>
<div>
<div className="text-xl font-bold text-white">{league.engagement}%</div>
<div className="text-xs text-gray-400">Engagement</div>
</div>
</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-racing-red/10">
<Calendar className="w-5 h-5 text-racing-red" />
</div>
<div>
<div className="text-xl font-bold text-white">{league.racesLeft}</div>
<div className="text-xs text-gray-400">Races Left</div>
</div>
</div>
</Card>
</motion.div>
</div>
{/* Tabs */}
<div className="flex gap-1 mb-6 border-b border-charcoal-outline overflow-x-auto">
{(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-3 text-sm font-medium capitalize transition-colors border-b-2 -mb-px whitespace-nowrap ${
activeTab === tab
? 'text-primary-blue border-primary-blue'
: 'text-gray-400 border-transparent hover:text-white'
}`}
>
{tab === 'sponsor' ? '🎯 Become a Sponsor' : tab}
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-5">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Trophy className="w-5 h-5 text-primary-blue" />
League Information
</h3>
<div className="space-y-3">
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Platform</span>
<span className="text-white font-medium">{league.game}</span>
</div>
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Season</span>
<span className="text-white font-medium">{league.season}</span>
</div>
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Duration</span>
<span className="text-white font-medium">Oct 2025 - Feb 2026</span>
</div>
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Drivers</span>
<span className="text-white font-medium">{league.drivers}</span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-400">Races</span>
<span className="text-white font-medium">{league.races}</span>
</div>
</div>
</Card>
<Card className="p-5">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-performance-green" />
Sponsorship Value
</h3>
<div className="space-y-3">
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Total Season Views</span>
<span className="text-white font-medium">{league.formattedTotalImpressions}</span>
</div>
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Projected Total</span>
<span className="text-white font-medium">{league.formattedProjectedTotal}</span>
</div>
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Main Sponsor CPM</span>
<span className="text-performance-green font-medium">
{league.formattedMainSponsorCpm}
</span>
</div>
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
<span className="text-gray-400">Engagement Rate</span>
<span className="text-white font-medium">{league.engagement}%</span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-400">League Rating</span>
<div className="flex items-center gap-1">
<Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />
<span className="text-white font-medium">{league.rating}/5.0</span>
</div>
</div>
</div>
</Card>
{/* Next Race */}
{league.nextRace && (
<Card className="p-5 lg:col-span-2">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Flag className="w-5 h-5 text-warning-amber" />
Next Race
</h3>
<div className="flex items-center justify-between p-4 rounded-lg bg-warning-amber/10 border border-warning-amber/30">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-lg bg-warning-amber/20 flex items-center justify-center">
<Flag className="w-6 h-6 text-warning-amber" />
</div>
<div>
<p className="font-semibold text-white text-lg">{league.nextRace.name}</p>
<p className="text-sm text-gray-400">{league.nextRace.date}</p>
</div>
</div>
<Button variant="secondary">
View Schedule
</Button>
</div>
</Card>
)}
</div>
)}
{activeTab === 'drivers' && (
<Card>
<div className="p-4 border-b border-charcoal-outline">
<h3 className="text-lg font-semibold text-white">Championship Standings</h3>
<p className="text-sm text-gray-400">Top drivers carrying sponsor branding</p>
</div>
<div className="divide-y divide-charcoal-outline/50">
{data.drivers.map((driver) => (
<div key={driver.id} className="flex items-center justify-between p-4 hover:bg-iron-gray/30 transition-colors">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-iron-gray flex items-center justify-center text-lg font-bold text-white">
{driver.position}
</div>
<div>
<div className="font-medium text-white">{driver.name}</div>
<div className="text-sm text-gray-500">{driver.team} {driver.country}</div>
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
<div className="font-medium text-white">{driver.races}</div>
<div className="text-xs text-gray-500">races</div>
</div>
<div className="text-right">
<div className="font-semibold text-white">{driver.formattedImpressions}</div>
<div className="text-xs text-gray-500">views</div>
</div>
</div>
</div>
))}
</div>
</Card>
)}
{activeTab === 'races' && (
<Card>
<div className="p-4 border-b border-charcoal-outline">
<h3 className="text-lg font-semibold text-white">Race Calendar</h3>
<p className="text-sm text-gray-400">Season schedule with view statistics</p>
</div>
<div className="divide-y divide-charcoal-outline/50">
{data.races.map((race) => (
<div key={race.id} className="flex items-center justify-between p-4 hover:bg-iron-gray/30 transition-colors">
<div className="flex items-center gap-4">
<div className={`w-3 h-3 rounded-full ${
race.status === 'completed' ? 'bg-performance-green' : 'bg-warning-amber'
}`} />
<div>
<div className="font-medium text-white">{race.name}</div>
<div className="text-sm text-gray-500">{race.formattedDate}</div>
</div>
</div>
<div className="flex items-center gap-4">
{race.status === 'completed' ? (
<div className="text-right">
<div className="font-semibold text-white">{race.views.toLocaleString()}</div>
<div className="text-xs text-gray-500">views</div>
</div>
) : (
<span className="px-3 py-1 rounded-full text-xs font-medium bg-warning-amber/20 text-warning-amber">
Upcoming
</span>
)}
</div>
</div>
))}
</div>
</Card>
)}
{activeTab === 'sponsor' && (
<div className="space-y-6">
{/* Tier Selection */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Main Sponsor */}
<Card
className={`p-5 cursor-pointer transition-all ${
selectedTier === 'main'
? 'border-primary-blue ring-2 ring-primary-blue/20'
: 'hover:border-charcoal-outline/80'
} ${!league.sponsorSlots.main.available ? 'opacity-60' : ''}`}
onClick={() => league.sponsorSlots.main.available && setSelectedTier('main')}
>
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Trophy className="w-5 h-5 text-yellow-400" />
<h3 className="text-lg font-semibold text-white">Main Sponsor</h3>
</div>
<p className="text-sm text-gray-400">Primary branding position</p>
</div>
{league.sponsorSlots.main.available ? (
<span className="px-2 py-1 rounded text-xs font-medium bg-performance-green/20 text-performance-green">
Available
</span>
) : (
<span className="px-2 py-1 rounded text-xs font-medium bg-gray-500/20 text-gray-400">
Filled
</span>
)}
</div>
<div className="text-3xl font-bold text-white mb-4">
${league.sponsorSlots.main.price}
<span className="text-sm font-normal text-gray-500">/season</span>
</div>
<ul className="space-y-2 mb-4">
{league.sponsorSlots.main.benefits.map((benefit: string, i: number) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-300">
<CheckCircle2 className="w-4 h-4 text-performance-green flex-shrink-0" />
{benefit}
</li>
))}
</ul>
{selectedTier === 'main' && league.sponsorSlots.main.available && (
<div className="w-4 h-4 rounded-full bg-primary-blue absolute top-4 right-4 flex items-center justify-center">
<CheckCircle2 className="w-3 h-3 text-white" />
</div>
)}
</Card>
{/* Secondary Sponsor */}
<Card
className={`p-5 cursor-pointer transition-all ${
selectedTier === 'secondary'
? 'border-primary-blue ring-2 ring-primary-blue/20'
: 'hover:border-charcoal-outline/80'
} ${league.sponsorSlots.secondary.available === 0 ? 'opacity-60' : ''}`}
onClick={() => league.sponsorSlots.secondary.available > 0 && setSelectedTier('secondary')}
>
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Star className="w-5 h-5 text-purple-400" />
<h3 className="text-lg font-semibold text-white">Secondary Sponsor</h3>
</div>
<p className="text-sm text-gray-400">Supporting branding position</p>
</div>
{league.sponsorSlots.secondary.available > 0 ? (
<span className="px-2 py-1 rounded text-xs font-medium bg-performance-green/20 text-performance-green">
{league.sponsorSlots.secondary.available}/{league.sponsorSlots.secondary.total} Available
</span>
) : (
<span className="px-2 py-1 rounded text-xs font-medium bg-gray-500/20 text-gray-400">
Full
</span>
)}
</div>
<div className="text-3xl font-bold text-white mb-4">
${league.sponsorSlots.secondary.price}
<span className="text-sm font-normal text-gray-500">/season</span>
</div>
<ul className="space-y-2 mb-4">
{league.sponsorSlots.secondary.benefits.map((benefit: string, i: number) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-300">
<CheckCircle2 className="w-4 h-4 text-performance-green flex-shrink-0" />
{benefit}
</li>
))}
</ul>
{selectedTier === 'secondary' && league.sponsorSlots.secondary.available > 0 && (
<div className="w-4 h-4 rounded-full bg-primary-blue absolute top-4 right-4 flex items-center justify-center">
<CheckCircle2 className="w-3 h-3 text-white" />
</div>
)}
</Card>
</div>
{/* Checkout Summary */}
<Card className="p-5">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<CreditCard className="w-5 h-5 text-primary-blue" />
Sponsorship Summary
</h3>
<div className="space-y-3 mb-6">
<div className="flex justify-between py-2">
<span className="text-gray-400">Selected Tier</span>
<span className="text-white font-medium capitalize">{selectedTier} Sponsor</span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-400">Season Price</span>
<span className="text-white font-medium">
${selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price}
</span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-400">Platform Fee ({siteConfig.fees.platformFeePercent}%)</span>
<span className="text-white font-medium">
${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}
</span>
</div>
<div className="flex justify-between py-2 border-t border-charcoal-outline pt-4">
<span className="text-white font-semibold">Total (excl. VAT)</span>
<span className="text-white font-bold text-xl">
${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * (1 + siteConfig.fees.platformFeePercent / 100)).toFixed(2)}
</span>
</div>
</div>
<p className="text-xs text-gray-500 mb-4">
{siteConfig.vat.notice}
</p>
<div className="flex gap-3">
<Button variant="primary" className="flex-1">
<Megaphone className="w-4 h-4 mr-2" />
Request Sponsorship
</Button>
<Button variant="secondary">
<FileText className="w-4 h-4 mr-2" />
Download Info Pack
</Button>
</div>
</Card>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,426 @@
'use client';
import { useState, useMemo } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import Link from 'next/link';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { siteConfig } from '@/lib/siteConfig';
import {
Trophy,
Users,
Eye,
Search,
Star,
ChevronRight,
Filter,
Car,
Flag,
TrendingUp,
CheckCircle2,
Clock,
Megaphone,
ArrowUpDown
} from 'lucide-react';
interface AvailableLeague {
id: string;
name: string;
game: string;
drivers: number;
avgViewsPerRace: number;
mainSponsorSlot: { available: boolean; price: number };
secondarySlots: { available: number; total: number; price: number };
rating: number;
tier: 'premium' | 'standard' | 'starter';
nextRace?: string;
seasonStatus: 'active' | 'upcoming' | 'completed';
description: string;
formattedAvgViews: string;
formattedCpm: string;
cpm: number;
tierConfig: any;
statusConfig: any;
}
type SortOption = 'rating' | 'drivers' | 'price' | 'views';
type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
type AvailabilityFilter = 'all' | 'main' | 'secondary';
interface SponsorLeaguesTemplateProps {
data: {
leagues: AvailableLeague[];
stats: {
total: number;
mainAvailable: number;
secondaryAvailable: number;
totalDrivers: number;
avgCpm: number;
};
};
}
function LeagueCard({ league, index }: { league: AvailableLeague; index: number }) {
const shouldReduceMotion = useReducedMotion();
const tierConfig = {
premium: {
bg: 'bg-gradient-to-br from-yellow-500/10 to-amber-500/5',
border: 'border-yellow-500/30',
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
icon: '⭐'
},
standard: {
bg: 'bg-gradient-to-br from-primary-blue/10 to-cyan-500/5',
border: 'border-primary-blue/30',
badge: 'bg-primary-blue/20 text-primary-blue border-primary-blue/30',
icon: '🏆'
},
starter: {
bg: 'bg-gradient-to-br from-gray-500/10 to-slate-500/5',
border: 'border-gray-500/30',
badge: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
icon: '🚀'
},
};
const statusConfig = {
active: { color: 'text-performance-green', bg: 'bg-performance-green/10', label: 'Active Season' },
upcoming: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', label: 'Starting Soon' },
completed: { color: 'text-gray-400', bg: 'bg-gray-400/10', label: 'Season Ended' },
};
const config = league.tierConfig;
const status = league.statusConfig;
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Card className={`overflow-hidden border ${config.border} ${config.bg} hover:border-primary-blue/50 transition-all duration-300 h-full`}>
<div className="p-5">
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`px-2 py-0.5 rounded text-xs font-medium capitalize border ${config.badge}`}>
{config.icon} {league.tier}
</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${status.bg} ${status.color}`}>
{status.label}
</span>
</div>
<h3 className="font-semibold text-white text-lg">{league.name}</h3>
<p className="text-sm text-gray-500">{league.game}</p>
</div>
<div className="flex items-center gap-1 bg-iron-gray/50 px-2 py-1 rounded">
<Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />
<span className="text-sm font-medium text-white">{league.rating}</span>
</div>
</div>
{/* Description */}
<p className="text-sm text-gray-400 mb-4 line-clamp-2">{league.description}</p>
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-2 mb-4">
<div className="text-center p-2 bg-iron-gray/50 rounded-lg">
<div className="text-lg font-bold text-white">{league.drivers}</div>
<div className="text-xs text-gray-500">Drivers</div>
</div>
<div className="text-center p-2 bg-iron-gray/50 rounded-lg">
<div className="text-lg font-bold text-white">{league.formattedAvgViews}</div>
<div className="text-xs text-gray-500">Avg Views</div>
</div>
<div className="text-center p-2 bg-iron-gray/50 rounded-lg">
<div className="text-lg font-bold text-performance-green">{league.formattedCpm}</div>
<div className="text-xs text-gray-500">CPM</div>
</div>
</div>
{/* Next Race */}
{league.nextRace && (
<div className="flex items-center gap-2 mb-4 text-sm">
<Clock className="w-4 h-4 text-gray-500" />
<span className="text-gray-400">Next:</span>
<span className="text-white">{league.nextRace}</span>
</div>
)}
{/* Sponsorship Slots */}
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between p-2.5 bg-iron-gray/30 rounded-lg">
<div className="flex items-center gap-2">
<div className={`w-2.5 h-2.5 rounded-full ${league.mainSponsorSlot.available ? 'bg-performance-green' : 'bg-racing-red'}`} />
<span className="text-sm text-gray-300">Main Sponsor</span>
</div>
<div className="text-sm">
{league.mainSponsorSlot.available ? (
<span className="text-white font-semibold">${league.mainSponsorSlot.price}/season</span>
) : (
<span className="text-gray-500 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3" /> Filled
</span>
)}
</div>
</div>
<div className="flex items-center justify-between p-2.5 bg-iron-gray/30 rounded-lg">
<div className="flex items-center gap-2">
<div className={`w-2.5 h-2.5 rounded-full ${league.secondarySlots.available > 0 ? 'bg-performance-green' : 'bg-racing-red'}`} />
<span className="text-sm text-gray-300">Secondary Slots</span>
</div>
<div className="text-sm">
{league.secondarySlots.available > 0 ? (
<span className="text-white font-semibold">
{league.secondarySlots.available}/{league.secondarySlots.total} @ ${league.secondarySlots.price}
</span>
) : (
<span className="text-gray-500 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3" /> Full
</span>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<Link href={`/sponsor/leagues/${league.id}`} className="flex-1">
<Button variant="secondary" className="w-full text-sm">
View Details
</Button>
</Link>
{(league.mainSponsorSlot.available || league.secondarySlots.available > 0) && (
<Link href={`/sponsor/leagues/${league.id}?action=sponsor`} className="flex-1">
<Button variant="primary" className="w-full text-sm">
Sponsor
</Button>
</Link>
)}
</div>
</div>
</Card>
</motion.div>
);
}
export function SponsorLeaguesTemplate({ data }: SponsorLeaguesTemplateProps) {
const shouldReduceMotion = useReducedMotion();
const [searchQuery, setSearchQuery] = useState('');
const [tierFilter, setTierFilter] = useState<TierFilter>('all');
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>('all');
const [sortBy, setSortBy] = useState<SortOption>('rating');
// Filter and sort leagues
const filteredLeagues = data.leagues
.filter((league: any) => {
if (searchQuery && !league.name.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
if (tierFilter !== 'all' && league.tier !== tierFilter) {
return false;
}
if (availabilityFilter === 'main' && !league.mainSponsorSlot.available) {
return false;
}
if (availabilityFilter === 'secondary' && league.secondarySlots.available === 0) {
return false;
}
return true;
})
.sort((a: any, b: any) => {
switch (sortBy) {
case 'rating': return b.rating - a.rating;
case 'drivers': return b.drivers - a.drivers;
case 'price': return a.mainSponsorSlot.price - b.mainSponsorSlot.price;
case 'views': return b.avgViewsPerRace - a.avgViewsPerRace;
default: return 0;
}
});
const stats = data.stats;
return (
<div className="max-w-7xl mx-auto py-8 px-4">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-gray-400 mb-6">
<Link href="/sponsor/dashboard" className="hover:text-white transition-colors">Dashboard</Link>
<ChevronRight className="w-4 h-4" />
<span className="text-white">Browse Leagues</span>
</div>
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-white mb-2 flex items-center gap-3">
<Trophy className="w-7 h-7 text-primary-blue" />
League Sponsorship Marketplace
</h1>
<p className="text-gray-400">
Discover racing leagues looking for sponsors. All prices shown exclude VAT.
</p>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="p-4 text-center">
<div className="text-2xl font-bold text-white">{stats.total}</div>
<div className="text-sm text-gray-400">Leagues</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className="p-4 text-center">
<div className="text-2xl font-bold text-performance-green">{stats.mainAvailable}</div>
<div className="text-sm text-gray-400">Main Slots</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="p-4 text-center">
<div className="text-2xl font-bold text-primary-blue">{stats.secondaryAvailable}</div>
<div className="text-sm text-gray-400">Secondary Slots</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Card className="p-4 text-center">
<div className="text-2xl font-bold text-white">{stats.totalDrivers}</div>
<div className="text-sm text-gray-400">Total Drivers</div>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Card className="p-4 text-center">
<div className="text-2xl font-bold text-warning-amber">${stats.avgCpm}</div>
<div className="text-sm text-gray-400">Avg CPM</div>
</Card>
</motion.div>
</div>
{/* Filters */}
<div className="flex flex-col lg:flex-row gap-4 mb-6">
{/* Search */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="text"
placeholder="Search leagues..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white placeholder-gray-500 focus:border-primary-blue focus:outline-none"
/>
</div>
{/* Tier Filter */}
<select
value={tierFilter}
onChange={(e) => setTierFilter(e.target.value as TierFilter)}
className="px-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white focus:border-primary-blue focus:outline-none"
>
<option value="all">All Tiers</option>
<option value="premium"> Premium</option>
<option value="standard">🏆 Standard</option>
<option value="starter">🚀 Starter</option>
</select>
{/* Availability Filter */}
<select
value={availabilityFilter}
onChange={(e) => setAvailabilityFilter(e.target.value as AvailabilityFilter)}
className="px-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white focus:border-primary-blue focus:outline-none"
>
<option value="all">All Slots</option>
<option value="main">Main Available</option>
<option value="secondary">Secondary Available</option>
</select>
{/* Sort */}
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as SortOption)}
className="px-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white focus:border-primary-blue focus:outline-none"
>
<option value="rating">Sort by Rating</option>
<option value="drivers">Sort by Drivers</option>
<option value="views">Sort by Views</option>
<option value="price">Sort by Price</option>
</select>
</div>
{/* Results Count */}
<div className="flex items-center justify-between mb-6">
<p className="text-sm text-gray-400">
Showing {filteredLeagues.length} of {data.leagues.length} leagues
</p>
<div className="flex items-center gap-2">
<Link href="/teams">
<Button variant="secondary" className="text-sm">
<Users className="w-4 h-4 mr-2" />
Browse Teams
</Button>
</Link>
<Link href="/drivers">
<Button variant="secondary" className="text-sm">
<Car className="w-4 h-4 mr-2" />
Browse Drivers
</Button>
</Link>
</div>
</div>
{/* League Grid */}
{filteredLeagues.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredLeagues.map((league: any, index: number) => (
<LeagueCard key={league.id} league={league} index={index} />
))}
</div>
) : (
<Card className="text-center py-16">
<Trophy className="w-12 h-12 text-gray-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">No leagues found</h3>
<p className="text-gray-400 mb-6">Try adjusting your filters to see more results</p>
<Button variant="secondary" onClick={() => {
setSearchQuery('');
setTierFilter('all');
setAvailabilityFilter('all');
}}>
Clear Filters
</Button>
</Card>
)}
{/* Platform Fee Notice */}
<div className="mt-8 rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-start gap-3">
<Megaphone className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-gray-300 font-medium mb-1">Platform Fee</p>
<p className="text-xs text-gray-500">
A {siteConfig.fees.platformFeePercent}% platform fee applies to all sponsorship payments. {siteConfig.fees.description}
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,158 @@
'use client';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import PendingSponsorshipRequests from '@/components/sponsors/PendingSponsorshipRequests';
import Card from '@/components/ui/Card';
import { AlertTriangle, Building, ChevronRight, Handshake, Trophy, User, Users } from 'lucide-react';
import Link from 'next/link';
export interface EntitySection {
entityType: 'driver' | 'team' | 'race' | 'season';
entityId: string;
entityName: string;
requests: any[];
}
export interface SponsorshipRequestsTemplateProps {
data: EntitySection[];
onAccept: (requestId: string) => Promise<void>;
onReject: (requestId: string, reason?: string) => Promise<void>;
}
export function SponsorshipRequestsTemplate({ data, onAccept, onReject }: SponsorshipRequestsTemplateProps) {
const totalRequests = data.reduce((sum, s) => sum + s.requests.length, 0);
const getEntityIcon = (type: 'driver' | 'team' | 'race' | 'season') => {
switch (type) {
case 'driver':
return User;
case 'team':
return Users;
case 'race':
return Trophy;
case 'season':
return Trophy;
default:
return Building;
}
};
const getEntityLink = (type: 'driver' | 'team' | 'race' | 'season', id: string) => {
switch (type) {
case 'driver':
return `/drivers/${id}`;
case 'team':
return `/teams/${id}`;
case 'race':
return `/races/${id}`;
case 'season':
return `/leagues/${id}/sponsorships`;
default:
return '#';
}
};
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<Breadcrumbs
items={[
{ label: 'Profile', href: '/profile' },
{ label: 'Sponsorship Requests' },
]}
/>
{/* Header */}
<div className="flex items-center gap-4 mt-6 mb-8">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-performance-green/10 border border-performance-green/30">
<Handshake className="w-7 h-7 text-performance-green" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Sponsorship Requests</h1>
<p className="text-sm text-gray-400">
Manage sponsorship requests for your profile, teams, and leagues
</p>
</div>
{totalRequests > 0 && (
<div className="ml-auto px-3 py-1 rounded-full bg-performance-green/20 text-performance-green text-sm font-semibold">
{totalRequests} pending
</div>
)}
</div>
{data.length === 0 ? (
<Card>
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-iron-gray/50 flex items-center justify-center">
<Handshake className="w-8 h-8 text-gray-500" />
</div>
<h3 className="text-lg font-medium text-white mb-2">No Pending Requests</h3>
<p className="text-sm text-gray-400">
You don't have any pending sponsorship requests at the moment.
</p>
<p className="text-xs text-gray-500 mt-2">
Sponsors can apply to sponsor your profile, teams, or leagues you manage.
</p>
</div>
</Card>
) : (
<div className="space-y-6">
{data.map((section) => {
const Icon = getEntityIcon(section.entityType);
const entityLink = getEntityLink(section.entityType, section.entityId);
return (
<Card key={`${section.entityType}-${section.entityId}`}>
{/* Section Header */}
<div className="flex items-center justify-between mb-6 pb-4 border-b border-charcoal-outline">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-iron-gray/50">
<Icon className="w-5 h-5 text-gray-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">{section.entityName}</h2>
<p className="text-xs text-gray-500 capitalize">{section.entityType}</p>
</div>
</div>
<Link
href={entityLink}
className="flex items-center gap-1 text-sm text-primary-blue hover:text-primary-blue/80 transition-colors"
>
View {section.entityType === 'season' ? 'Sponsorships' : section.entityType}
<ChevronRight className="w-4 h-4" />
</Link>
</div>
{/* Requests */}
<PendingSponsorshipRequests
entityType={section.entityType}
entityId={section.entityId}
entityName={section.entityName}
requests={section.requests}
onAccept={onAccept}
onReject={onReject}
/>
</Card>
);
})}
</div>
)}
{/* Info Card */}
<Card className="mt-8 bg-gradient-to-r from-primary-blue/5 to-transparent border-primary-blue/20">
<div className="flex items-start gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/20 flex-shrink-0">
<Building className="w-5 h-5 text-primary-blue" />
</div>
<div>
<h3 className="text-sm font-semibold text-white mb-1">How Sponsorships Work</h3>
<p className="text-xs text-gray-400 leading-relaxed">
Sponsors can apply to sponsor your driver profile, teams you manage, or leagues you administer.
Review each request carefully - accepting will activate the sponsorship and the sponsor will be
charged. You'll receive the payment minus a 10% platform fee.
</p>
</div>
</div>
</Card>
</div>
);
}