wip
This commit is contained in:
41
apps/website/app/auth/iracing/callback/route.ts
Normal file
41
apps/website/app/auth/iracing/callback/route.ts
Normal 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);
|
||||
}
|
||||
30
apps/website/app/auth/iracing/page.tsx
Normal file
30
apps/website/app/auth/iracing/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
apps/website/app/auth/iracing/start/route.ts
Normal file
23
apps/website/app/auth/iracing/start/route.ts
Normal 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);
|
||||
}
|
||||
11
apps/website/app/auth/logout/route.ts
Normal file
11
apps/website/app/auth/logout/route.ts
Normal 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);
|
||||
}
|
||||
73
apps/website/app/dashboard/page.tsx
Normal file
73
apps/website/app/dashboard/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user