This commit is contained in:
2025-12-04 11:54:23 +01:00
parent c0fdae3d3c
commit 9d5caa87f3
83 changed files with 1579 additions and 2151 deletions

View File

@@ -0,0 +1,41 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
import { getAuthService } from '../../../../lib/auth';
const SESSION_COOKIE = 'gp_demo_session';
const STATE_COOKIE = 'gp_demo_auth_state';
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get('code') ?? undefined;
const state = url.searchParams.get('state') ?? undefined;
const returnTo = url.searchParams.get('returnTo') ?? undefined;
if (!code || !state) {
return NextResponse.redirect('/auth/iracing');
}
const cookieStore = await cookies();
const storedState = cookieStore.get(STATE_COOKIE)?.value;
if (!storedState || storedState !== state) {
return NextResponse.redirect('/auth/iracing');
}
const authService = getAuthService();
const session = await authService.loginWithIracingCallback({ code, state, returnTo });
cookieStore.set(SESSION_COOKIE, JSON.stringify(session), {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
});
cookieStore.delete(STATE_COOKIE);
const redirectTarget = returnTo || '/dashboard';
const absoluteRedirect = new URL(redirectTarget, url.origin).toString();
return NextResponse.redirect(absoluteRedirect);
}

View File

@@ -0,0 +1,30 @@
import Link from 'next/link';
interface IracingAuthPageProps {
searchParams: Promise<{
returnTo?: string;
}>;
}
export default async function IracingAuthPage({ searchParams }: IracingAuthPageProps) {
const params = await searchParams;
const returnTo = params.returnTo ?? '/dashboard';
const startUrl = `/auth/iracing/start?returnTo=${encodeURIComponent(returnTo)}`;
return (
<main className="min-h-screen flex items-center justify-center bg-deep-graphite">
<div className="max-w-md w-full px-6 py-8 bg-iron-gray/80 rounded-lg border border-white/10 shadow-xl">
<h1 className="text-2xl font-semibold text-white mb-4">Authenticate with iRacing</h1>
<p className="text-sm text-gray-300 mb-6">
Connect a demo iRacing identity to explore the GridPilot dashboard with seeded data.
</p>
<Link
href={startUrl}
className="inline-flex items-center justify-center px-4 py-2 rounded-md bg-primary-blue text-sm font-medium text-white hover:bg-primary-blue/90 transition-colors w-full"
>
Start iRacing demo login
</Link>
</div>
</main>
);
}

View File

@@ -0,0 +1,23 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
import { getAuthService } from '../../../../lib/auth';
export async function GET(request: Request) {
const url = new URL(request.url);
const returnTo = url.searchParams.get('returnTo') ?? undefined;
const authService = getAuthService();
const { redirectUrl, state } = await authService.startIracingAuthRedirect(returnTo);
const cookieStore = await cookies();
cookieStore.set('gp_demo_auth_state', state, {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
});
const absoluteRedirect = new URL(redirectUrl, url.origin).toString();
return NextResponse.redirect(absoluteRedirect);
}

View File

@@ -0,0 +1,11 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const cookieStore = await cookies();
cookieStore.delete('gp_demo_session');
const url = new URL(request.url);
const redirectUrl = new URL('/', url.origin);
return NextResponse.redirect(redirectUrl);
}

View File

@@ -0,0 +1,73 @@
import { redirect } from 'next/navigation';
import FeedLayout from '@/components/feed/FeedLayout';
import { getAuthService } from '@/lib/auth';
import {
getFeedRepository,
getRaceRepository,
getResultRepository,
} from '@/lib/di-container';
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const authService = getAuthService();
const session = await authService.getCurrentSession();
if (!session) {
redirect('/auth/iracing?returnTo=/dashboard');
}
const feedRepository = getFeedRepository();
const raceRepository = getRaceRepository();
const resultRepository = getResultRepository();
const [feedItems, upcomingRaces, allResults] = await Promise.all([
feedRepository.getFeedForDriver(session.user.primaryDriverId ?? ''),
raceRepository.findAll(),
resultRepository.findAll(),
]);
const upcoming = upcomingRaces
.filter((race) => race.status === 'scheduled')
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime())
.slice(0, 5);
const completedRaces = upcomingRaces.filter((race) => race.status === 'completed');
const latestResults = completedRaces.slice(0, 4).map((race) => {
const raceResults = allResults.filter((result) => result.raceId === race.id);
const winner = raceResults.sort((a, b) => a.position - b.position)[0];
return {
raceId: race.id,
leagueId: race.leagueId,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
winnerDriverId: winner?.driverId ?? '',
winnerName: 'Race Winner',
positionChange: winner ? winner.getPositionChange() : 0,
};
});
return (
<main className="min-h-screen bg-deep-graphite">
<section className="max-w-7xl mx-auto px-6 pt-10 pb-4">
<div className="flex items-baseline justify-between gap-4 mb-4">
<div>
<h1 className="text-3xl font-bold text-white">Dashboard</h1>
<p className="text-sm text-gray-400">
Personalized activity from your friends, leagues, and teams.
</p>
</div>
</div>
</section>
<FeedLayout
feedItems={feedItems}
upcomingRaces={upcoming}
latestResults={latestResults}
/>
</main>
);
}

View File

@@ -3,11 +3,11 @@
import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { getDriverRepository } from '@/lib/di-container';
import DriverProfile from '@/components/alpha/DriverProfile';
import DriverProfile from '@/components/drivers/DriverProfile';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Breadcrumbs from '@/components/alpha/Breadcrumbs';
import { EntityMappers, DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import { EntityMappers, DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
export default function DriverDetailPage() {
const router = useRouter();

View File

@@ -2,16 +2,16 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import DriverCard from '@/components/alpha/DriverCard';
import RankBadge from '@/components/alpha/RankBadge';
import DriverCard from '@/components/drivers/DriverCard';
import RankBadge from '@/components/drivers/RankBadge';
import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card';
// Mock data
// Mock data (fictional demo drivers only)
const MOCK_DRIVERS = [
{
id: '1',
name: 'Max Verstappen',
name: 'Alex Vermeer',
rating: 3245,
skillLevel: 'pro' as const,
nationality: 'Netherlands',
@@ -23,7 +23,7 @@ const MOCK_DRIVERS = [
},
{
id: '2',
name: 'Lewis Hamilton',
name: 'Liam Hartmann',
rating: 3198,
skillLevel: 'pro' as const,
nationality: 'United Kingdom',

View File

@@ -1,10 +1,14 @@
import React from 'react';
import type { Metadata } from 'next';
import './globals.css';
import { getAppMode } from '@/lib/mode';
import { getAuthService } from '@/lib/auth';
import { AlphaNav } from '@/components/alpha/AlphaNav';
import AlphaBanner from '@/components/alpha/AlphaBanner';
import AlphaFooter from '@/components/alpha/AlphaFooter';
export const dynamic = 'force-dynamic';
export const metadata: Metadata = {
title: 'GridPilot - iRacing League Racing Platform',
description: 'The dedicated home for serious iRacing leagues. Automatic results, standings, team racing, and professional race control.',
@@ -32,7 +36,7 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
@@ -40,13 +44,17 @@ export default function RootLayout({
const mode = getAppMode();
if (mode === 'alpha') {
const authService = getAuthService();
const session = await authService.getCurrentSession();
const isAuthenticated = !!session;
return (
<html lang="en" className="scroll-smooth overflow-x-hidden">
<head>
<meta name="mobile-web-app-capable" content="yes" />
</head>
<body className="antialiased overflow-x-hidden min-h-screen bg-deep-graphite flex flex-col">
<AlphaNav />
<AlphaNav isAuthenticated={isAuthenticated} />
<AlphaBanner />
<main className="flex-1 max-w-7xl mx-auto px-6 py-8 w-full">
{children}

View File

@@ -5,20 +5,19 @@ import { useRouter, useParams } from 'next/navigation';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import FeatureLimitationTooltip from '@/components/alpha/FeatureLimitationTooltip';
import JoinLeagueButton from '@/components/alpha/JoinLeagueButton';
import MembershipStatus from '@/components/alpha/MembershipStatus';
import LeagueMembers from '@/components/alpha/LeagueMembers';
import LeagueSchedule from '@/components/alpha/LeagueSchedule';
import LeagueAdmin from '@/components/alpha/LeagueAdmin';
import StandingsTable from '@/components/alpha/StandingsTable';
import DataWarning from '@/components/alpha/DataWarning';
import Breadcrumbs from '@/components/alpha/Breadcrumbs';
import { League } from '@gridpilot/racing-domain/entities/League';
import { Standing } from '@gridpilot/racing-domain/entities/Standing';
import { Race } from '@gridpilot/racing-domain/entities/Race';
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
import JoinLeagueButton from '@/components/leagues/JoinLeagueButton';
import MembershipStatus from '@/components/leagues/MembershipStatus';
import LeagueMembers from '@/components/leagues/LeagueMembers';
import LeagueSchedule from '@/components/leagues/LeagueSchedule';
import LeagueAdmin from '@/components/leagues/LeagueAdmin';
import StandingsTable from '@/components/leagues/StandingsTable';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { getLeagueRepository, getRaceRepository, getDriverRepository, getStandingRepository } from '@/lib/di-container';
import { getMembership, isOwnerOrAdmin, getCurrentDriverId } from '@/lib/membership-data';
import { getMembership, isOwnerOrAdmin, getCurrentDriverId } from '@gridpilot/racing/application';
export default function LeagueDetailPage() {
const router = useRouter();
@@ -142,8 +141,6 @@ export default function LeagueDetailPage() {
<p className="text-gray-400">{league.description}</p>
</div>
<DataWarning />
{/* Action Card */}
{!membership && (
<Card className="mb-6">

View File

@@ -4,10 +4,10 @@ import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import StandingsTable from '@/components/alpha/StandingsTable';
import { League } from '@gridpilot/racing-domain/entities/League';
import { Standing } from '@gridpilot/racing-domain/entities/Standing';
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
import StandingsTable from '@/components/leagues/StandingsTable';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import {
getLeagueRepository,
getStandingRepository,

View File

@@ -2,12 +2,12 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import LeagueCard from '@/components/alpha/LeagueCard';
import CreateLeagueForm from '@/components/alpha/CreateLeagueForm';
import LeagueCard from '@/components/leagues/LeagueCard';
import CreateLeagueForm from '@/components/leagues/CreateLeagueForm';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input';
import { League } from '@gridpilot/racing-domain/entities/League';
import { League } from '@gridpilot/racing/domain/entities/League';
import { getLeagueRepository } from '@/lib/di-container';
export default function LeaguesPage() {

View File

@@ -1,15 +1,7 @@
'use client';
import { redirect } from 'next/navigation';
import { getAppMode } from '@/lib/mode';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import CompanionStatus from '@/components/alpha/CompanionStatus';
import DataWarning from '@/components/alpha/DataWarning';
import RaceCard from '@/components/alpha/RaceCard';
import LeagueCard from '@/components/alpha/LeagueCard';
import TeamCard from '@/components/alpha/TeamCard';
import { getAuthService } from '@/lib/auth';
import Hero from '@/components/landing/Hero';
import AlternatingSection from '@/components/landing/AlternatingSection';
import FeatureGrid from '@/components/landing/FeatureGrid';
@@ -21,628 +13,347 @@ 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 { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
import { getAllTeams, getTeamMembers } from '@/lib/team-data';
import { getLeagueMembers } from '@/lib/membership-data';
import type { Race } from '@gridpilot/racing-domain/entities/Race';
import type { League } from '@gridpilot/racing-domain/entities/League';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import {
topLeagues,
teams,
getUpcomingRaces,
} from '@gridpilot/testing-support';
function AlphaDashboard() {
const router = useRouter();
const [upcomingRaces, setUpcomingRaces] = useState<Race[]>([]);
const [topLeagues, setTopLeagues] = useState<League[]>([]);
const [featuredTeams, setFeaturedTeams] = useState<any[]>([]);
const [recentActivity, setRecentActivity] = useState<any[]>([]);
export default async function HomePage() {
const authService = getAuthService();
const session = await authService.getCurrentSession();
useEffect(() => {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
if (session) {
redirect('/dashboard');
}
// Get upcoming races
raceRepo.findAll().then(races => {
const upcoming = races
.filter(r => r.status === 'scheduled')
.sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())
.slice(0, 5);
setUpcomingRaces(upcoming);
});
const mode = getAppMode();
const isAlpha = mode === 'alpha';
const upcomingRaces = getUpcomingRaces(3);
// Get top leagues
leagueRepo.findAll().then(leagues => {
const sorted = leagues
.map(league => ({
league,
memberCount: getLeagueMembers(league.id).length,
}))
.sort((a, b) => b.memberCount - a.memberCount)
.slice(0, 4)
.map(item => item.league);
setTopLeagues(sorted);
});
// Get featured teams
const teams = getAllTeams();
const featured = teams
.map(team => ({
...team,
memberCount: getTeamMembers(team.id).length,
}))
.sort((a, b) => b.memberCount - a.memberCount)
.slice(0, 4);
setFeaturedTeams(featured);
// Generate recent activity
const activities = [
{ type: 'race', text: 'Max Verstappen won at Monza GP', time: '2 hours ago' },
{ type: 'join', text: 'Lando Norris joined European GT Championship', time: '5 hours ago' },
{ type: 'team', text: 'Charles Leclerc joined Weekend Warriors', time: '1 day ago' },
{ type: 'race', text: 'Upcoming: Spa-Francorchamps in 2 days', time: '2 days ago' },
{ type: 'league', text: 'European GT Championship: 4 active members', time: '3 days ago' },
];
setRecentActivity(activities);
}, []);
return (
<div className="max-w-7xl mx-auto">
<DataWarning />
{/* Upcoming Races Section */}
{upcomingRaces.length > 0 && (
<div className="mb-12">
<div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold text-white">Upcoming Races</h2>
<Button variant="secondary" onClick={() => router.push('/races')}>
View All Races
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{upcomingRaces.slice(0, 3).map(race => (
<RaceCard
key={race.id}
race={race}
onClick={() => router.push(`/races/${race.id}`)}
/>
))}
</div>
</div>
)}
{/* Top Leagues Section */}
{topLeagues.length > 0 && (
<div className="mb-12">
<div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold text-white">Top Leagues</h2>
<Button variant="secondary" onClick={() => router.push('/leagues')}>
Browse Leagues
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{topLeagues.map(league => (
<LeagueCard
key={league.id}
league={league}
onClick={() => router.push(`/leagues/${league.id}`)}
/>
))}
</div>
</div>
)}
{/* Featured Teams Section */}
{featuredTeams.length > 0 && (
<div className="mb-12">
<div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold text-white">Featured Teams</h2>
<Button variant="secondary" onClick={() => router.push('/teams')}>
Browse Teams
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{featuredTeams.map(team => (
<TeamCard
key={team.id}
id={team.id}
name={team.name}
logo={undefined}
memberCount={team.memberCount}
leagues={team.leagues}
onClick={() => router.push(`/teams/${team.id}`)}
/>
))}
</div>
</div>
)}
{/* Recent Activity Section */}
{recentActivity.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-bold text-white mb-6">Recent Activity</h2>
<Card>
<div className="space-y-4">
{recentActivity.map((activity, idx) => (
<div
key={idx}
className={`flex items-start gap-3 pb-4 ${
idx < recentActivity.length - 1 ? 'border-b border-charcoal-outline' : ''
}`}
>
<div className="w-2 h-2 rounded-full bg-primary-blue mt-2 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm text-gray-300">{activity.text}</p>
<p className="text-xs text-gray-500 mt-1">{activity.time}</p>
</div>
</div>
))}
</div>
</Card>
</div>
)}
<div className="max-w-4xl mx-auto">
{/* Welcome Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-white mb-4">GridPilot Alpha</h1>
<p className="text-gray-400 text-lg">
Complete workflow prototype. Test freely all data is temporary.
</p>
</div>
{/* Companion Status */}
<div className="mb-8">
<CompanionStatus />
</div>
{/* What's in Alpha */}
<Card className="mb-8">
<h2 className="text-2xl font-semibold text-white mb-4">What's in Alpha</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-performance-green flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-300">Driver profile creation</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-performance-green flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-300">League management</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-performance-green flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-300">Race scheduling</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-performance-green flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-300">CSV result import</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-performance-green flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-300">Championship standings</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-performance-green flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-gray-300">Full workflow end-to-end</span>
</div>
</div>
</Card>
{/* What's Coming */}
<Card className="mb-8 bg-iron-gray">
<h2 className="text-2xl font-semibold text-white mb-4">What's Coming</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">Persistent data storage</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">Automated session creation</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">Automated result import</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">Multi-league memberships</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">Team championships</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">Advanced statistics</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">Social features</span>
</div>
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span className="text-gray-300">League discovery</span>
</div>
</div>
</Card>
{/* Known Limitations */}
<Card className="mb-8 border border-warning-amber/20 bg-iron-gray">
<div className="flex items-start gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-warning-amber/10 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-warning-amber" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white mb-2">Known Limitations</h3>
<ul className="space-y-2 text-sm text-gray-400">
<li className="flex items-start gap-2">
<span className="text-warning-amber mt-0.5">•</span>
<span>Data resets on page reload (in-memory only)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-warning-amber mt-0.5">•</span>
<span>Manual iRacing session creation required</span>
</li>
<li className="flex items-start gap-2">
<span className="text-warning-amber mt-0.5">•</span>
<span>Manual CSV result upload required</span>
</li>
<li className="flex items-start gap-2">
<span className="text-warning-amber mt-0.5">•</span>
<span>Single league membership per driver</span>
</li>
<li className="flex items-start gap-2">
<span className="text-warning-amber mt-0.5">•</span>
<span>No user authentication</span>
</li>
<li className="flex items-start gap-2">
<span className="text-warning-amber mt-0.5">•</span>
<span>iRacing platform only</span>
</li>
</ul>
</div>
</div>
</Card>
{/* Quick Start Guide */}
<Card className="mb-8">
<h2 className="text-2xl font-semibold text-white mb-4">Quick Start Guide</h2>
<div className="space-y-4">
<div className="flex items-start gap-4">
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-primary-blue/20 text-primary-blue font-semibold flex-shrink-0">
1
</span>
<div>
<h3 className="text-white font-medium mb-1">Create Your Profile</h3>
<p className="text-sm text-gray-400">Set up your driver profile with racing number and iRacing ID.</p>
<Button
variant="secondary"
onClick={() => router.push('/profile')}
className="mt-2"
>
Go to Profile
</Button>
</div>
</div>
<div className="flex items-start gap-4 pt-4 border-t border-charcoal-outline">
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-charcoal-outline text-gray-400 font-semibold flex-shrink-0">
2
</span>
<div>
<h3 className="text-white font-medium mb-1">Join or Create a League</h3>
<p className="text-sm text-gray-400">Browse available leagues or create your own.</p>
<Button
variant="secondary"
onClick={() => router.push('/leagues')}
className="mt-2"
>
Browse Leagues
</Button>
</div>
</div>
<div className="flex items-start gap-4 pt-4 border-t border-charcoal-outline">
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-charcoal-outline text-gray-400 font-semibold flex-shrink-0">
3
</span>
<div>
<h3 className="text-white font-medium mb-1">Schedule Races</h3>
<p className="text-sm text-gray-400">Create race events and manage your schedule.</p>
<Button
variant="secondary"
onClick={() => router.push('/races')}
className="mt-2"
>
View Races
</Button>
</div>
</div>
</div>
</Card>
{/* Navigation Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div onClick={() => router.push('/profile')} className="cursor-pointer">
<Card className="hover:border-primary-blue/30 transition-colors">
<div className="flex flex-col items-center text-center py-4">
<div className="w-12 h-12 rounded-lg bg-primary-blue/10 flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-primary-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h3 className="text-white font-semibold mb-1">Profile</h3>
<p className="text-sm text-gray-400">Manage your driver profile</p>
</div>
</Card>
</div>
<div onClick={() => router.push('/leagues')} className="cursor-pointer">
<Card className="hover:border-primary-blue/30 transition-colors">
<div className="flex flex-col items-center text-center py-4">
<div className="w-12 h-12 rounded-lg bg-primary-blue/10 flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-primary-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<h3 className="text-white font-semibold mb-1">Leagues</h3>
<p className="text-sm text-gray-400">Browse and join leagues</p>
</div>
</Card>
</div>
<div onClick={() => router.push('/races')} className="cursor-pointer">
<Card className="hover:border-primary-blue/30 transition-colors">
<div className="flex flex-col items-center text-center py-4">
<div className="w-12 h-12 rounded-lg bg-primary-blue/10 flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-primary-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-white font-semibold mb-1">Races</h3>
<p className="text-sm text-gray-400">View race schedule</p>
</div>
</Card>
</div>
</div>
</div>
</div>
);
}
function LandingPage() {
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>
<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>
<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 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>
<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 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-green-600/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(34,197,94,0.2)]">
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-green-600/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-green-600/25 to-green-900/25 border border-green-600/40 flex items-center justify-center shadow-lg group-hover:shadow-green-600/20 group-hover:scale-110 transition-all">
<svg className="w-4 h-4 md:w-5 md:h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">Runs on your machine, totally transparent, completely safe</span>
<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>
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
Automation instead of repetition.
</p>
</>
}
mockup={<CompanionAutomationMockup />}
layout="text-left"
/>
</div>
<p className="mt-4">
iRacing gives you physics. GridPilot gives you a career.
</p>
</>
}
mockup={<CareerProgressionMockup />}
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.
<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 */}
{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>
<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"
/>
</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">
{topLeagues.slice(0, 4).map(league => (
<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 => 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">
{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 text-xs font-semibold text-white">
{team.tag}
</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>
{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">
{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.scheduledAt.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric'
})}
</div>
</li>
))}
</ul>
)}
</Card>
</div>
</section>
)}
<DiscordCTA />
<FAQ />
<Footer />
</main>
);
}
export default function HomePage() {
const mode = getAppMode();
if (mode === 'alpha') {
return <AlphaDashboard />;
}
return <LandingPage />;
}

View File

@@ -3,19 +3,18 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { getDriverRepository } from '@/lib/di-container';
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
import { EntityMappers, DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
import CreateDriverForm from '@/components/alpha/CreateDriverForm';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { EntityMappers, DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import DataWarning from '@/components/alpha/DataWarning';
import ProfileHeader from '@/components/alpha/ProfileHeader';
import ProfileStats from '@/components/alpha/ProfileStats';
import ProfileRaceHistory from '@/components/alpha/ProfileRaceHistory';
import ProfileSettings from '@/components/alpha/ProfileSettings';
import CareerHighlights from '@/components/alpha/CareerHighlights';
import RatingBreakdown from '@/components/alpha/RatingBreakdown';
import { getDriverTeam, getCurrentDriverId } from '@/lib/team-data';
import ProfileHeader from '@/components/profile/ProfileHeader';
import ProfileStats from '@/components/drivers/ProfileStats';
import ProfileRaceHistory from '@/components/drivers/ProfileRaceHistory';
import ProfileSettings from '@/components/drivers/ProfileSettings';
import CareerHighlights from '@/components/drivers/CareerHighlights';
import RatingBreakdown from '@/components/drivers/RatingBreakdown';
import { getDriverTeam, getCurrentDriverId } from '@gridpilot/racing/application';
type Tab = 'overview' | 'statistics' | 'history' | 'settings';
@@ -95,8 +94,6 @@ export default function ProfilePage() {
return (
<div className="max-w-6xl mx-auto">
<DataWarning className="mb-6" />
<Card className="mb-6">
<ProfileHeader
driver={driver}

View File

@@ -5,21 +5,21 @@ import { useRouter, useParams } from 'next/navigation';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import FeatureLimitationTooltip from '@/components/alpha/FeatureLimitationTooltip';
import { Race } from '@gridpilot/racing-domain/entities/Race';
import { League } from '@gridpilot/racing-domain/entities/League';
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
import type { Race } from '@gridpilot/racing/domain/entities/Race';
import type { League } from '@gridpilot/racing/domain/entities/League';
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { getRaceRepository, getLeagueRepository, getDriverRepository } from '@/lib/di-container';
import { getMembership, getCurrentDriverId } from '@/lib/membership-data';
import {
getMembership,
getCurrentDriverId,
isRegistered,
registerForRace,
withdrawFromRace,
getRegisteredDrivers
} from '@/lib/registration-data';
getRegisteredDrivers,
} from '@gridpilot/racing/application';
import CompanionStatus from '@/components/alpha/CompanionStatus';
import CompanionInstructions from '@/components/alpha/CompanionInstructions';
import DataWarning from '@/components/alpha/DataWarning';
import Breadcrumbs from '@/components/alpha/Breadcrumbs';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
export default function RaceDetailPage() {
const router = useRouter();
@@ -73,7 +73,9 @@ export default function RaceDetailPage() {
const drivers = await Promise.all(
registeredDriverIds.map(id => driverRepo.findById(id))
);
setEntryList(drivers.filter((d): d is Driver => d !== null));
setEntryList(
drivers.filter((d: Driver | null): d is Driver => d !== null)
);
// Check user registration status
const userIsRegistered = isRegistered(raceId, currentDriverId);
@@ -186,7 +188,7 @@ export default function RaceDetailPage() {
scheduled: 'bg-primary-blue/20 text-primary-blue border-primary-blue/30',
completed: 'bg-green-500/20 text-green-400 border-green-500/30',
cancelled: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
};
} as const;
if (loading) {
return (
@@ -250,7 +252,11 @@ export default function RaceDetailPage() {
<p className="text-gray-400">{league.name}</p>
)}
</div>
<span className={`px-3 py-1 text-sm font-medium rounded border ${statusColors[race.status]}`}>
<span
className={`px-3 py-1 text-sm font-medium rounded border ${
statusColors[race.status as keyof typeof statusColors]
}`}
>
{race.status.charAt(0).toUpperCase() + race.status.slice(1)}
</span>
</div>
@@ -391,8 +397,6 @@ export default function RaceDetailPage() {
</span>
</div>
<DataWarning className="mb-4" />
{entryList.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<p className="mb-2">No drivers registered yet</p>

View File

@@ -4,12 +4,12 @@ import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import ResultsTable from '@/components/alpha/ResultsTable';
import ImportResultsForm from '@/components/alpha/ImportResultsForm';
import { Race } from '@gridpilot/racing-domain/entities/Race';
import { League } from '@gridpilot/racing-domain/entities/League';
import { Result } from '@gridpilot/racing-domain/entities/Result';
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
import ResultsTable from '@/components/races/ResultsTable';
import ImportResultsForm from '@/components/races/ImportResultsForm';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Result } from '@gridpilot/racing/domain/entities/Result';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import {
getRaceRepository,
getLeagueRepository,

View File

@@ -4,10 +4,10 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import RaceCard from '@/components/alpha/RaceCard';
import ScheduleRaceForm from '@/components/alpha/ScheduleRaceForm';
import { Race, RaceStatus } from '@gridpilot/racing-domain/entities/Race';
import { League } from '@gridpilot/racing-domain/entities/League';
import RaceCard from '@/components/races/RaceCard';
import ScheduleRaceForm from '@/components/leagues/ScheduleRaceForm';
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
import { League } from '@gridpilot/racing/domain/entities/League';
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
export default function RacesPage() {

View File

@@ -1,170 +0,0 @@
'use client';
import Card from '@/components/ui/Card';
// Mock data for highlights
const MOCK_HIGHLIGHTS = [
{
id: '1',
type: 'race',
title: 'Epic finish in GT3 Championship',
description: 'Max Verstappen wins by 0.003 seconds',
time: '2 hours ago',
},
{
id: '2',
type: 'league',
title: 'New league created: Endurance Masters',
description: '12 teams already registered',
time: '5 hours ago',
},
{
id: '3',
type: 'achievement',
title: 'Sarah Chen unlocked "Century Club"',
description: '100 races completed',
time: '1 day ago',
},
];
const TRENDING_DRIVERS = [
{ id: '1', name: 'Max Verstappen', metric: '+156 rating this week' },
{ id: '2', name: 'Emma Thompson', metric: '5 wins in a row' },
{ id: '3', name: 'Lewis Hamilton', metric: 'Most laps led' },
];
const TRENDING_TEAMS = [
{ id: '1', name: 'Apex Racing', metric: '12 new members' },
{ id: '2', name: 'Speed Demons', metric: '3 championship wins' },
{ id: '3', name: 'Endurance Elite', metric: '24h race victory' },
];
export default function SocialPage() {
return (
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Social Hub</h1>
<p className="text-gray-400">
Stay updated with the racing community
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Activity Feed */}
<div className="lg:col-span-2">
<Card>
<div className="space-y-6">
<h2 className="text-xl font-semibold text-white">
Activity Feed
</h2>
<div className="bg-primary-blue/10 border border-primary-blue/20 rounded-lg p-8 text-center">
<div className="text-4xl mb-4">🚧</div>
<h3 className="text-lg font-semibold text-white mb-2">
Coming Soon
</h3>
<p className="text-gray-400">
The activity feed will show real-time updates from your
friends, leagues, and teams. This feature is currently in
development for the alpha release.
</p>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-400 mb-4">
Recent Highlights
</h3>
<div className="space-y-4">
{MOCK_HIGHLIGHTS.map((highlight) => (
<div
key={highlight.id}
className="border-l-4 border-primary-blue pl-4 py-2"
>
<h4 className="font-semibold text-white">
{highlight.title}
</h4>
<p className="text-sm text-gray-400">
{highlight.description}
</p>
<p className="text-xs text-gray-500 mt-1">
{highlight.time}
</p>
</div>
))}
</div>
</div>
</div>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Trending Drivers */}
<Card>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-white">
🔥 Trending Drivers
</h2>
<div className="space-y-3">
{TRENDING_DRIVERS.map((driver, index) => (
<div key={driver.id} className="flex items-center gap-3">
<div className="w-8 h-8 bg-charcoal-outline rounded-full flex items-center justify-center text-sm font-bold text-gray-400">
{index + 1}
</div>
<div className="flex-1">
<div className="font-medium text-white">
{driver.name}
</div>
<div className="text-xs text-gray-400">
{driver.metric}
</div>
</div>
</div>
))}
</div>
</div>
</Card>
{/* Trending Teams */}
<Card>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-white">
Trending Teams
</h2>
<div className="space-y-3">
{TRENDING_TEAMS.map((team, index) => (
<div key={team.id} className="flex items-center gap-3">
<div className="w-8 h-8 bg-charcoal-outline rounded-lg flex items-center justify-center text-sm font-bold text-gray-400">
{index + 1}
</div>
<div className="flex-1">
<div className="font-medium text-white">
{team.name}
</div>
<div className="text-xs text-gray-400">
{team.metric}
</div>
</div>
</div>
))}
</div>
</div>
</Card>
{/* Friend Activity Placeholder */}
<Card>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-white">
Friends
</h2>
<div className="bg-charcoal-outline rounded-lg p-4 text-center">
<p className="text-sm text-gray-400">
Friend features coming soon in alpha
</p>
</div>
</div>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -4,12 +4,11 @@ import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import DataWarning from '@/components/alpha/DataWarning';
import Breadcrumbs from '@/components/alpha/Breadcrumbs';
import TeamRoster from '@/components/alpha/TeamRoster';
import TeamStandings from '@/components/alpha/TeamStandings';
import TeamAdmin from '@/components/alpha/TeamAdmin';
import JoinTeamButton from '@/components/alpha/JoinTeamButton';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import TeamRoster from '@/components/teams/TeamRoster';
import TeamStandings from '@/components/teams/TeamStandings';
import TeamAdmin from '@/components/teams/TeamAdmin';
import JoinTeamButton from '@/components/teams/JoinTeamButton';
import {
Team,
getTeam,
@@ -20,7 +19,7 @@ import {
removeTeamMember,
updateTeamMemberRole,
TeamRole,
} from '@/lib/team-data';
} from '@gridpilot/racing/application';
type Tab = 'overview' | 'roster' | 'standings' | 'admin';
@@ -129,8 +128,6 @@ export default function TeamDetailPage() {
]}
/>
<DataWarning className="mb-6" />
<Card className="mb-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-6">

View File

@@ -2,13 +2,12 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import TeamCard from '@/components/alpha/TeamCard';
import TeamCard from '@/components/teams/TeamCard';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card';
import CreateTeamForm from '@/components/alpha/CreateTeamForm';
import DataWarning from '@/components/alpha/DataWarning';
import { getAllTeams, getTeamMembers, Team } from '@/lib/team-data';
import CreateTeamForm from '@/components/teams/CreateTeamForm';
import { getAllTeams, getTeamMembers, type Team } from '@gridpilot/racing/application';
export default function TeamsPage() {
const router = useRouter();
@@ -52,7 +51,6 @@ export default function TeamsPage() {
if (showCreateForm) {
return (
<div className="max-w-4xl mx-auto">
<DataWarning className="mb-6" />
<div className="mb-6">
<Button
@@ -75,9 +73,7 @@ export default function TeamsPage() {
}
return (
<div className="max-w-6xl mx-auto">
<DataWarning className="mb-6" />
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Teams</h1>