From 9d5caa87f314de7c1d871aeae01563ead1281485 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 4 Dec 2025 11:54:23 +0100 Subject: [PATCH] wip --- .../app/auth/iracing/callback/route.ts | 41 + apps/website/app/auth/iracing/page.tsx | 30 + apps/website/app/auth/iracing/start/route.ts | 23 + apps/website/app/auth/logout/route.ts | 11 + apps/website/app/dashboard/page.tsx | 73 ++ apps/website/app/drivers/[id]/page.tsx | 6 +- apps/website/app/drivers/page.tsx | 10 +- apps/website/app/layout.tsx | 12 +- apps/website/app/leagues/[id]/page.tsx | 27 +- .../app/leagues/[id]/standings/page.tsx | 8 +- apps/website/app/leagues/page.tsx | 6 +- apps/website/app/page.tsx | 933 ++++++------------ apps/website/app/profile/page.tsx | 23 +- apps/website/app/races/[id]/page.tsx | 30 +- apps/website/app/races/[id]/results/page.tsx | 12 +- apps/website/app/races/page.tsx | 8 +- apps/website/app/social/page.tsx | 170 ---- apps/website/app/teams/[id]/page.tsx | 15 +- apps/website/app/teams/page.tsx | 12 +- apps/website/components/alpha/AlphaFooter.tsx | 9 +- apps/website/components/alpha/AlphaNav.tsx | 51 +- apps/website/components/alpha/Breadcrumbs.tsx | 57 -- .../alpha/CompanionInstructions.tsx | 2 +- apps/website/components/alpha/DataWarning.tsx | 56 -- apps/website/components/alpha/DriverCard.tsx | 99 -- .../components/alpha/DriverRankings.tsx | 77 -- .../{alpha => drivers}/CareerHighlights.tsx | 0 .../{alpha => drivers}/CreateDriverForm.tsx | 6 +- .../website/components/drivers/DriverCard.tsx | 73 ++ .../{alpha => drivers}/DriverProfile.tsx | 6 +- .../components/drivers/DriverRankings.tsx | 81 ++ .../{alpha => drivers}/PerformanceMetrics.tsx | 0 .../{alpha => drivers}/ProfileRaceHistory.tsx | 0 .../{alpha => drivers}/ProfileSettings.tsx | 2 +- .../{alpha => drivers}/ProfileStats.tsx | 0 .../{alpha => drivers}/RankBadge.tsx | 0 .../{alpha => drivers}/RatingBreakdown.tsx | 0 .../components/feed/FeedEmptyState.tsx | 25 + apps/website/components/feed/FeedItemCard.tsx | 83 ++ apps/website/components/feed/FeedLayout.tsx | 43 + apps/website/components/feed/FeedList.tsx | 21 + .../website/components/layout/Breadcrumbs.tsx | 53 + .../{alpha => leagues}/CreateLeagueForm.tsx | 4 +- .../{alpha => leagues}/JoinLeagueButton.tsx | 2 +- .../{alpha => leagues}/LeagueAdmin.tsx | 9 +- .../{alpha => leagues}/LeagueCard.tsx | 2 +- .../{alpha => leagues}/LeagueMembers.tsx | 9 +- .../{alpha => leagues}/LeagueSchedule.tsx | 8 +- .../{alpha => leagues}/MembershipStatus.tsx | 2 +- .../{alpha => leagues}/ScheduleRaceForm.tsx | 10 +- .../{alpha => leagues}/StandingsTable.tsx | 4 +- .../{alpha => profile}/ProfileHeader.tsx | 4 +- .../{alpha => races}/ImportResultsForm.tsx | 4 +- .../components/races/LatestResultsSidebar.tsx | 36 + .../components/{alpha => races}/RaceCard.tsx | 2 +- .../{alpha => races}/ResultsTable.tsx | 4 +- .../components/races/UpcomingRacesSidebar.tsx | 45 + .../{alpha => teams}/CreateTeamForm.tsx | 2 +- .../{alpha => teams}/JoinTeamButton.tsx | 2 +- .../components/{alpha => teams}/TeamAdmin.tsx | 4 +- .../components/{alpha => teams}/TeamCard.tsx | 0 .../{alpha => teams}/TeamRoster.tsx | 4 +- .../{alpha => teams}/TeamStandings.tsx | 4 +- apps/website/lib/auth/AuthContext.tsx | 121 +++ apps/website/lib/auth/AuthService.ts | 27 + apps/website/lib/auth/InMemoryAuthService.ts | 96 ++ apps/website/lib/auth/index.ts | 11 + apps/website/lib/di-container.ts | 274 ++--- apps/website/lib/email-validation.ts | 53 - apps/website/lib/membership-data.ts | 208 ---- apps/website/lib/registration-data.ts | 130 --- apps/website/lib/team-data.ts | 335 ------- apps/website/package.json | 5 + .../public/images/avatars/avatar-1.svg | 11 + .../public/images/avatars/avatar-2.svg | 11 + .../public/images/avatars/avatar-3.svg | 11 + .../public/images/avatars/avatar-4.svg | 11 + .../public/images/avatars/avatar-5.svg | 11 + .../public/images/avatars/avatar-6.svg | 11 + apps/website/public/images/logos/team-1.svg | 13 + apps/website/public/images/logos/team-2.svg | 12 + apps/website/public/images/logos/team-3.svg | 12 + apps/website/public/images/logos/team-4.svg | 12 + 83 files changed, 1579 insertions(+), 2151 deletions(-) create mode 100644 apps/website/app/auth/iracing/callback/route.ts create mode 100644 apps/website/app/auth/iracing/page.tsx create mode 100644 apps/website/app/auth/iracing/start/route.ts create mode 100644 apps/website/app/auth/logout/route.ts create mode 100644 apps/website/app/dashboard/page.tsx delete mode 100644 apps/website/app/social/page.tsx delete mode 100644 apps/website/components/alpha/Breadcrumbs.tsx delete mode 100644 apps/website/components/alpha/DataWarning.tsx delete mode 100644 apps/website/components/alpha/DriverCard.tsx delete mode 100644 apps/website/components/alpha/DriverRankings.tsx rename apps/website/components/{alpha => drivers}/CareerHighlights.tsx (100%) rename apps/website/components/{alpha => drivers}/CreateDriverForm.tsx (97%) create mode 100644 apps/website/components/drivers/DriverCard.tsx rename apps/website/components/{alpha => drivers}/DriverProfile.tsx (97%) create mode 100644 apps/website/components/drivers/DriverRankings.tsx rename apps/website/components/{alpha => drivers}/PerformanceMetrics.tsx (100%) rename apps/website/components/{alpha => drivers}/ProfileRaceHistory.tsx (100%) rename apps/website/components/{alpha => drivers}/ProfileSettings.tsx (99%) rename apps/website/components/{alpha => drivers}/ProfileStats.tsx (100%) rename apps/website/components/{alpha => drivers}/RankBadge.tsx (100%) rename apps/website/components/{alpha => drivers}/RatingBreakdown.tsx (100%) create mode 100644 apps/website/components/feed/FeedEmptyState.tsx create mode 100644 apps/website/components/feed/FeedItemCard.tsx create mode 100644 apps/website/components/feed/FeedLayout.tsx create mode 100644 apps/website/components/feed/FeedList.tsx create mode 100644 apps/website/components/layout/Breadcrumbs.tsx rename apps/website/components/{alpha => leagues}/CreateLeagueForm.tsx (97%) rename apps/website/components/{alpha => leagues}/JoinLeagueButton.tsx (99%) rename apps/website/components/{alpha => leagues}/LeagueAdmin.tsx (97%) rename apps/website/components/{alpha => leagues}/LeagueCard.tsx (94%) rename apps/website/components/{alpha => leagues}/LeagueMembers.tsx (97%) rename apps/website/components/{alpha => leagues}/LeagueSchedule.tsx (98%) rename apps/website/components/{alpha => leagues}/MembershipStatus.tsx (97%) rename apps/website/components/{alpha => leagues}/ScheduleRaceForm.tsx (96%) rename apps/website/components/{alpha => leagues}/StandingsTable.tsx (95%) rename apps/website/components/{alpha => profile}/ProfileHeader.tsx (95%) rename apps/website/components/{alpha => races}/ImportResultsForm.tsx (97%) create mode 100644 apps/website/components/races/LatestResultsSidebar.tsx rename apps/website/components/{alpha => races}/RaceCard.tsx (97%) rename apps/website/components/{alpha => races}/ResultsTable.tsx (96%) create mode 100644 apps/website/components/races/UpcomingRacesSidebar.tsx rename apps/website/components/{alpha => teams}/CreateTeamForm.tsx (98%) rename apps/website/components/{alpha => teams}/JoinTeamButton.tsx (98%) rename apps/website/components/{alpha => teams}/TeamAdmin.tsx (98%) rename apps/website/components/{alpha => teams}/TeamCard.tsx (100%) rename apps/website/components/{alpha => teams}/TeamRoster.tsx (98%) rename apps/website/components/{alpha => teams}/TeamStandings.tsx (97%) create mode 100644 apps/website/lib/auth/AuthContext.tsx create mode 100644 apps/website/lib/auth/AuthService.ts create mode 100644 apps/website/lib/auth/InMemoryAuthService.ts create mode 100644 apps/website/lib/auth/index.ts delete mode 100644 apps/website/lib/email-validation.ts delete mode 100644 apps/website/lib/membership-data.ts delete mode 100644 apps/website/lib/registration-data.ts delete mode 100644 apps/website/lib/team-data.ts create mode 100644 apps/website/public/images/avatars/avatar-1.svg create mode 100644 apps/website/public/images/avatars/avatar-2.svg create mode 100644 apps/website/public/images/avatars/avatar-3.svg create mode 100644 apps/website/public/images/avatars/avatar-4.svg create mode 100644 apps/website/public/images/avatars/avatar-5.svg create mode 100644 apps/website/public/images/avatars/avatar-6.svg create mode 100644 apps/website/public/images/logos/team-1.svg create mode 100644 apps/website/public/images/logos/team-2.svg create mode 100644 apps/website/public/images/logos/team-3.svg create mode 100644 apps/website/public/images/logos/team-4.svg diff --git a/apps/website/app/auth/iracing/callback/route.ts b/apps/website/app/auth/iracing/callback/route.ts new file mode 100644 index 000000000..8f64f98a5 --- /dev/null +++ b/apps/website/app/auth/iracing/callback/route.ts @@ -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); +} \ No newline at end of file diff --git a/apps/website/app/auth/iracing/page.tsx b/apps/website/app/auth/iracing/page.tsx new file mode 100644 index 000000000..bc366df5b --- /dev/null +++ b/apps/website/app/auth/iracing/page.tsx @@ -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 ( +
+
+

Authenticate with iRacing

+

+ Connect a demo iRacing identity to explore the GridPilot dashboard with seeded data. +

+ + Start iRacing demo login + +
+
+ ); +} \ No newline at end of file diff --git a/apps/website/app/auth/iracing/start/route.ts b/apps/website/app/auth/iracing/start/route.ts new file mode 100644 index 000000000..3302eaf57 --- /dev/null +++ b/apps/website/app/auth/iracing/start/route.ts @@ -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); +} \ No newline at end of file diff --git a/apps/website/app/auth/logout/route.ts b/apps/website/app/auth/logout/route.ts new file mode 100644 index 000000000..714ee281a --- /dev/null +++ b/apps/website/app/auth/logout/route.ts @@ -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); +} \ No newline at end of file diff --git a/apps/website/app/dashboard/page.tsx b/apps/website/app/dashboard/page.tsx new file mode 100644 index 000000000..0adaad7a6 --- /dev/null +++ b/apps/website/app/dashboard/page.tsx @@ -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 ( +
+
+
+
+

Dashboard

+

+ Personalized activity from your friends, leagues, and teams. +

+
+
+
+ +
+ ); +} \ No newline at end of file diff --git a/apps/website/app/drivers/[id]/page.tsx b/apps/website/app/drivers/[id]/page.tsx index a20e0d748..d9ac57498 100644 --- a/apps/website/app/drivers/[id]/page.tsx +++ b/apps/website/app/drivers/[id]/page.tsx @@ -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(); diff --git a/apps/website/app/drivers/page.tsx b/apps/website/app/drivers/page.tsx index 9dfccd6fc..f32cd28b6 100644 --- a/apps/website/app/drivers/page.tsx +++ b/apps/website/app/drivers/page.tsx @@ -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', diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index 4e3e96042..814fa292d 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -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 ( - +
{children} diff --git a/apps/website/app/leagues/[id]/page.tsx b/apps/website/app/leagues/[id]/page.tsx index 093c95d20..0a122483e 100644 --- a/apps/website/app/leagues/[id]/page.tsx +++ b/apps/website/app/leagues/[id]/page.tsx @@ -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() {

{league.description}

- - {/* Action Card */} {!membership && ( diff --git a/apps/website/app/leagues/[id]/standings/page.tsx b/apps/website/app/leagues/[id]/standings/page.tsx index e417d4336..0c64029a0 100644 --- a/apps/website/app/leagues/[id]/standings/page.tsx +++ b/apps/website/app/leagues/[id]/standings/page.tsx @@ -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, diff --git a/apps/website/app/leagues/page.tsx b/apps/website/app/leagues/page.tsx index 28ad03942..77c8ae573 100644 --- a/apps/website/app/leagues/page.tsx +++ b/apps/website/app/leagues/page.tsx @@ -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() { diff --git a/apps/website/app/page.tsx b/apps/website/app/page.tsx index dac446540..3fa6c6771 100644 --- a/apps/website/app/page.tsx +++ b/apps/website/app/page.tsx @@ -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([]); - const [topLeagues, setTopLeagues] = useState([]); - const [featuredTeams, setFeaturedTeams] = useState([]); - const [recentActivity, setRecentActivity] = useState([]); +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 ( -
- - - {/* Upcoming Races Section */} - {upcomingRaces.length > 0 && ( -
-
-

Upcoming Races

- -
-
- {upcomingRaces.slice(0, 3).map(race => ( - router.push(`/races/${race.id}`)} - /> - ))} -
-
- )} - - {/* Top Leagues Section */} - {topLeagues.length > 0 && ( -
-
-

Top Leagues

- -
-
- {topLeagues.map(league => ( - router.push(`/leagues/${league.id}`)} - /> - ))} -
-
- )} - - {/* Featured Teams Section */} - {featuredTeams.length > 0 && ( -
-
-

Featured Teams

- -
-
- {featuredTeams.map(team => ( - router.push(`/teams/${team.id}`)} - /> - ))} -
-
- )} - - {/* Recent Activity Section */} - {recentActivity.length > 0 && ( -
-

Recent Activity

- -
- {recentActivity.map((activity, idx) => ( -
-
-
-

{activity.text}

-

{activity.time}

-
-
- ))} -
- -
- )} - -
- {/* Welcome Header */} -
-

GridPilot Alpha

-

- Complete workflow prototype. Test freely — all data is temporary. -

-
- - {/* Companion Status */} -
- -
- - {/* What's in Alpha */} - -

What's in Alpha

-
-
- - - - Driver profile creation -
-
- - - - League management -
-
- - - - Race scheduling -
-
- - - - CSV result import -
-
- - - - Championship standings -
-
- - - - Full workflow end-to-end -
-
-
- - {/* What's Coming */} - -

What's Coming

-
-
- - - - Persistent data storage -
-
- - - - Automated session creation -
-
- - - - Automated result import -
-
- - - - Multi-league memberships -
-
- - - - Team championships -
-
- - - - Advanced statistics -
-
- - - - Social features -
-
- - - - League discovery -
-
-
- - {/* Known Limitations */} - -
-
- - - -
-
-

Known Limitations

-
    -
  • - - Data resets on page reload (in-memory only) -
  • -
  • - - Manual iRacing session creation required -
  • -
  • - - Manual CSV result upload required -
  • -
  • - - Single league membership per driver -
  • -
  • - - No user authentication -
  • -
  • - - iRacing platform only -
  • -
-
-
-
- - {/* Quick Start Guide */} - -

Quick Start Guide

-
-
- - 1 - -
-

Create Your Profile

-

Set up your driver profile with racing number and iRacing ID.

- -
-
- -
- - 2 - -
-

Join or Create a League

-

Browse available leagues or create your own.

- -
-
- -
- - 3 - -
-

Schedule Races

-

Create race events and manage your schedule.

- -
-
-
-
- - {/* Navigation Cards */} -
-
router.push('/profile')} className="cursor-pointer"> - -
-
- - - -
-

Profile

-

Manage your driver profile

-
-
-
- -
router.push('/leagues')} className="cursor-pointer"> - -
-
- - - -
-

Leagues

-

Browse and join leagues

-
-
-
- -
router.push('/races')} className="cursor-pointer"> - -
-
- - - -
-

Races

-

View race schedule

-
-
-
-
-
-
- ); -} - -function LandingPage() { return (
- - - {/* Section 1: A Persistent Identity */} - -

- Your races, your seasons, your progress — finally in one place. -

-
-
-
-
-
- - - -
- Lifetime stats and season history across all your leagues -
-
-
-
-
-
- - - -
- Track your performance, consistency, and team contributions -
-
-
-
-
-
- - - -
- Your own rating that reflects real league competition + + + {/* Section 1: A Persistent Identity */} + +

+ Your races, your seasons, your progress — finally in one place. +

+
+
+
+
+
+ + +
+ + Lifetime stats and season history across all your leagues +
-

- iRacing gives you physics. GridPilot gives you a career. -

- - } - mockup={} - layout="text-left" - /> - - - - {/* Section 2: Results That Actually Stay */} - -

- Every race you run stays with you. -

-
-
-
-
-
- - - -
- Your stats, your team, your story — all connected -
-
-
-
-
-
- - - -
- One race result updates your profile, team points, rating, and season history -
-
-
-
-
-
- - - -
- No more fragmented data across spreadsheets and forums +
+
+
+
+ + +
+ + Track your performance, consistency, and team contributions +
-

- Your racing career, finally in one place. -

- - } - mockup={} - layout="text-right" - /> - - {/* Section 3: Automatic Session Creation */} - -

- Setting up league races used to mean clicking through iRacing's wizard 20 times. -

-
-
-
-
-
- 1 -
- Our companion app syncs with your league schedule -
-
-
-
-
-
- 2 -
- When it's race time, it creates the iRacing session automatically -
-
-
-
-
-
- 3 -
- No clicking through wizards. No manual setup -
-
-
-
-
-
- - - -
- Runs on your machine, totally transparent, completely safe +
+
+
+
+ + +
+ + Your own rating that reflects real league competition +
-

- Automation instead of repetition. -

- - } - mockup={} - layout="text-left" - /> +
+

+ iRacing gives you physics. GridPilot gives you a career. +

+ + } + mockup={} + layout="text-left" + /> - {/* Section 4: Game-Agnostic Platform */} - -

- Right now, we're focused on making iRacing league racing better. + + + {/* Section 2: Results That Actually Stay */} + +

+ Every race you run stays with you. +

+
+
+
+
+
+ + + +
+ + Your stats, your team, your story — all connected + +
+
+
+
+
+
+ + + +
+ + One race result updates your profile, team points, rating, and season history + +
+
+
+
+
+
+ + + +
+ + No more fragmented data across spreadsheets and forums + +
+
+
+

+ Your racing career, finally in one place. +

+ + } + mockup={} + layout="text-right" + /> + + {/* Section 3: Automatic Session Creation */} + +

+ Setting up league races used to mean clicking through iRacing's wizard 20 times. +

+
+
+
+
+
+ 1 +
+ + Our companion app syncs with your league schedule + +
+
+
+
+
+
+ 2 +
+ + When it's race time, it creates the iRacing session automatically + +
+
+
+
+
+
+ 3 +
+ + No clicking through wizards. No manual setup + +
+
+
+

+ Automation instead of repetition. +

+ + } + mockup={} + layout="text-left" + /> + + {/* Section 4: Game-Agnostic Platform */} + +

+ Right now, we're focused on making iRacing league racing better. +

+

+ But sims come and go. Your leagues, your teams, your rating — those stay. +

+

+ GridPilot is built to outlast any single platform. +

+

+ When the next sim arrives, your competitive identity moves with you. +

+ + } + mockup={} + layout="text-right" + /> + + {/* Alpha-only discovery section */} + {isAlpha && ( +
+
+
+

Discover the grid

+

+ Explore leagues, teams, and races that make up the GridPilot ecosystem.

-

- But sims come and go. Your leagues, your teams, your rating — those stay. -

-

- GridPilot is built to outlast any single platform. -

-

- When the next sim arrives, your competitive identity moves with you. -

- - } - mockup={} - layout="text-right" - /> +
+
+ +
+ {/* Top leagues */} + +
+

Featured leagues

+ +
+
    + {topLeagues.slice(0, 4).map(league => ( +
  • +
    + {league.name + .split(' ') + .map(word => word[0]) + .join('') + .slice(0, 3) + .toUpperCase()} +
    +
    +

    {league.name}

    +

    + {league.description} +

    +
    +
  • + ))} +
+
+ + {/* Teams */} + +
+

Teams on the grid

+ +
+
    + {teams.slice(0, 4).map(team => ( +
  • +
    + {team.tag} +
    +
    +

    {team.name}

    +

    + {team.description} +

    +
    +
  • + ))} +
+
+ + {/* Upcoming races */} + +
+

Upcoming races

+ +
+ {upcomingRaces.length === 0 ? ( +

+ No races scheduled in this demo snapshot. +

+ ) : ( +
    + {upcomingRaces.map(race => ( +
  • +
    +

    {race.track}

    +

    {race.car}

    +
    +
    + {race.scheduledAt.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric' + })} +
    +
  • + ))} +
+ )} +
+
+
+ )}
); -} - -export default function HomePage() { - const mode = getAppMode(); - - if (mode === 'alpha') { - return ; - } - - return ; } \ No newline at end of file diff --git a/apps/website/app/profile/page.tsx b/apps/website/app/profile/page.tsx index e12c2b696..f861ea471 100644 --- a/apps/website/app/profile/page.tsx +++ b/apps/website/app/profile/page.tsx @@ -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 (
- - 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() {

{league.name}

)}
- + {race.status.charAt(0).toUpperCase() + race.status.slice(1)}
@@ -391,8 +397,6 @@ export default function RaceDetailPage() { - - {entryList.length === 0 ? (

No drivers registered yet

diff --git a/apps/website/app/races/[id]/results/page.tsx b/apps/website/app/races/[id]/results/page.tsx index bcc354bab..38deac6e0 100644 --- a/apps/website/app/races/[id]/results/page.tsx +++ b/apps/website/app/races/[id]/results/page.tsx @@ -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, diff --git a/apps/website/app/races/page.tsx b/apps/website/app/races/page.tsx index 58598b32b..8cdd2eeae 100644 --- a/apps/website/app/races/page.tsx +++ b/apps/website/app/races/page.tsx @@ -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() { diff --git a/apps/website/app/social/page.tsx b/apps/website/app/social/page.tsx deleted file mode 100644 index 5ae5733be..000000000 --- a/apps/website/app/social/page.tsx +++ /dev/null @@ -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 ( -
-
-

Social Hub

-

- Stay updated with the racing community -

-
- -
- {/* Activity Feed */} -
- -
-

- Activity Feed -

-
-
🚧
-

- Coming Soon -

-

- The activity feed will show real-time updates from your - friends, leagues, and teams. This feature is currently in - development for the alpha release. -

-
- -
-

- Recent Highlights -

-
- {MOCK_HIGHLIGHTS.map((highlight) => ( -
-

- {highlight.title} -

-

- {highlight.description} -

-

- {highlight.time} -

-
- ))} -
-
-
-
-
- - {/* Sidebar */} -
- {/* Trending Drivers */} - -
-

- 🔥 Trending Drivers -

-
- {TRENDING_DRIVERS.map((driver, index) => ( -
-
- {index + 1} -
-
-
- {driver.name} -
-
- {driver.metric} -
-
-
- ))} -
-
-
- - {/* Trending Teams */} - -
-

- ⭐ Trending Teams -

-
- {TRENDING_TEAMS.map((team, index) => ( -
-
- {index + 1} -
-
-
- {team.name} -
-
- {team.metric} -
-
-
- ))} -
-
-
- - {/* Friend Activity Placeholder */} - -
-

- Friends -

-
-

- Friend features coming soon in alpha -

-
-
-
-
-
-
- ); -} \ No newline at end of file diff --git a/apps/website/app/teams/[id]/page.tsx b/apps/website/app/teams/[id]/page.tsx index 00f74d5ed..dedc4855f 100644 --- a/apps/website/app/teams/[id]/page.tsx +++ b/apps/website/app/teams/[id]/page.tsx @@ -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() { ]} /> - -
diff --git a/apps/website/app/teams/page.tsx b/apps/website/app/teams/page.tsx index b62d59e75..4463f0c74 100644 --- a/apps/website/app/teams/page.tsx +++ b/apps/website/app/teams/page.tsx @@ -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 (
-
+ + )} +
+
-
); diff --git a/apps/website/components/alpha/Breadcrumbs.tsx b/apps/website/components/alpha/Breadcrumbs.tsx deleted file mode 100644 index 4371bc43f..000000000 --- a/apps/website/components/alpha/Breadcrumbs.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; - -export interface BreadcrumbItem { - label: string; - href?: string; -} - -interface BreadcrumbsProps { - items: BreadcrumbItem[]; -} - -export default function Breadcrumbs({ items }: BreadcrumbsProps) { - const router = useRouter(); - - return ( - - ); -} \ No newline at end of file diff --git a/apps/website/components/alpha/CompanionInstructions.tsx b/apps/website/components/alpha/CompanionInstructions.tsx index 3b1908f6e..30641658c 100644 --- a/apps/website/components/alpha/CompanionInstructions.tsx +++ b/apps/website/components/alpha/CompanionInstructions.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import Card from '../ui/Card'; import Button from '../ui/Button'; -import { Race } from '@gridpilot/racing-domain/entities/Race'; +import { Race } from '@gridpilot/racing/domain/entities/Race'; interface CompanionInstructionsProps { race: Race; diff --git a/apps/website/components/alpha/DataWarning.tsx b/apps/website/components/alpha/DataWarning.tsx deleted file mode 100644 index 5e8e53b9a..000000000 --- a/apps/website/components/alpha/DataWarning.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; - -interface DataWarningProps { - className?: string; -} - -export default function DataWarning({ className }: DataWarningProps) { - const [isDismissed, setIsDismissed] = useState(false); - const [isMounted, setIsMounted] = useState(false); - - useEffect(() => { - setIsMounted(true); - const dismissed = sessionStorage.getItem('data-warning-dismissed'); - if (dismissed === 'true') { - setIsDismissed(true); - } - }, []); - - const handleDismiss = () => { - sessionStorage.setItem('data-warning-dismissed', 'true'); - setIsDismissed(true); - }; - - if (!isMounted) return null; - if (isDismissed) return null; - - return ( -
-
-
-
- - - -
-
-

- Your data will be lost when you refresh the page. Alpha uses in-memory storage only. -

-
-
- -
-
- ); -} \ No newline at end of file diff --git a/apps/website/components/alpha/DriverCard.tsx b/apps/website/components/alpha/DriverCard.tsx deleted file mode 100644 index bace8fb65..000000000 --- a/apps/website/components/alpha/DriverCard.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client'; - -import Card from '../ui/Card'; - -interface DriverCardProps { - id: string; - name: string; - avatar?: string; - rating: number; - skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro'; - nationality?: string; - racesCompleted: number; - wins: number; - isActive?: boolean; - onClick?: () => void; -} - -export default function DriverCard({ - id, - name, - avatar, - rating, - skillLevel, - nationality, - racesCompleted, - wins, - isActive = true, - onClick, -}: DriverCardProps) { - const skillBadgeColors = { - beginner: 'bg-green-500/20 text-green-400', - intermediate: 'bg-blue-500/20 text-blue-400', - advanced: 'bg-purple-500/20 text-purple-400', - pro: 'bg-red-500/20 text-red-400', - }; - - return ( -
- -
-
-
-
- {avatar ? ( - {name} - ) : ( - - {name.charAt(0)} - - )} -
- {isActive && ( -
- )} -
-
-

- {name} -

- {nationality && ( -

{nationality}

- )} -
-
- -
-
-
{rating}
-
Rating
-
-
-
{wins}
-
Wins
-
-
-
{racesCompleted}
-
Races
-
-
- - - {skillLevel.charAt(0).toUpperCase() + skillLevel.slice(1)} - -
- -
- ); -} \ No newline at end of file diff --git a/apps/website/components/alpha/DriverRankings.tsx b/apps/website/components/alpha/DriverRankings.tsx deleted file mode 100644 index fb1d4f8a7..000000000 --- a/apps/website/components/alpha/DriverRankings.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client'; - -import Card from '../ui/Card'; -import RankBadge from './RankBadge'; - -interface RankingData { - type: 'overall' | 'league' | 'class'; - name: string; - rank: number; - totalDrivers: number; - percentile: number; - rating: number; -} - -interface DriverRankingsProps { - rankings: RankingData[]; -} - -export default function DriverRankings({ rankings }: DriverRankingsProps) { - const getPercentileColor = (percentile: number) => { - if (percentile >= 90) return 'text-green-400'; - if (percentile >= 75) return 'text-primary-blue'; - if (percentile >= 50) return 'text-warning-amber'; - return 'text-gray-400'; - }; - - const getPercentileLabel = (percentile: number) => { - if (percentile >= 90) return 'Top 10%'; - if (percentile >= 75) return 'Top 25%'; - if (percentile >= 50) return 'Top 50%'; - return `${(100 - percentile).toFixed(0)}th percentile`; - }; - - return ( - -

Rankings

- -
- {rankings.map((ranking, index) => ( -
-
-
- -
-
{ranking.name}
-
- {ranking.rank} of {ranking.totalDrivers} drivers -
-
-
-
-
{ranking.rating}
-
Rating
-
-
- -
- Percentile - - {getPercentileLabel(ranking.percentile)} - -
-
- ))} -
- - {rankings.length === 0 && ( -
- No ranking data available yet. -
- )} -
- ); -} \ No newline at end of file diff --git a/apps/website/components/alpha/CareerHighlights.tsx b/apps/website/components/drivers/CareerHighlights.tsx similarity index 100% rename from apps/website/components/alpha/CareerHighlights.tsx rename to apps/website/components/drivers/CareerHighlights.tsx diff --git a/apps/website/components/alpha/CreateDriverForm.tsx b/apps/website/components/drivers/CreateDriverForm.tsx similarity index 97% rename from apps/website/components/alpha/CreateDriverForm.tsx rename to apps/website/components/drivers/CreateDriverForm.tsx index 44cc06d73..1f7adea75 100644 --- a/apps/website/components/alpha/CreateDriverForm.tsx +++ b/apps/website/components/drivers/CreateDriverForm.tsx @@ -4,8 +4,7 @@ import { useState, FormEvent } from 'react'; import { useRouter } from 'next/navigation'; import Input from '../ui/Input'; import Button from '../ui/Button'; -import DataWarning from './DataWarning'; -import { Driver } from '@gridpilot/racing-domain/entities/Driver'; +import { Driver } from '@gridpilot/racing/domain/entities/Driver'; import { getDriverRepository } from '../../lib/di-container'; interface FormErrors { @@ -93,7 +92,6 @@ export default function CreateDriverForm() { return ( <> -
diff --git a/apps/website/components/drivers/DriverCard.tsx b/apps/website/components/drivers/DriverCard.tsx new file mode 100644 index 000000000..a76633aed --- /dev/null +++ b/apps/website/components/drivers/DriverCard.tsx @@ -0,0 +1,73 @@ +import Card from '@/components/ui/Card'; +import RankBadge from '@/components/drivers/RankBadge'; + +export interface DriverCardProps { + id: string; + name: string; + rating: number; + skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro'; + nationality: string; + racesCompleted: number; + wins: number; + podiums: number; + rank: number; + onClick?: () => void; +} + +export default function DriverCard(props: DriverCardProps) { + const { + name, + rating, + nationality, + racesCompleted, + wins, + podiums, + rank, + onClick, + } = props; + + return ( + +
+
+ + +
+ {name.charAt(0)} +
+ +
+

{name}

+

+ {nationality} • {racesCompleted} races +

+
+
+ +
+
+
{rating}
+
Rating
+
+
+
{wins}
+
Wins
+
+
+
{podiums}
+
Podiums
+
+
+
+ {racesCompleted > 0 ? ((wins / racesCompleted) * 100).toFixed(0) : '0'}% +
+
Win Rate
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/website/components/alpha/DriverProfile.tsx b/apps/website/components/drivers/DriverProfile.tsx similarity index 97% rename from apps/website/components/alpha/DriverProfile.tsx rename to apps/website/components/drivers/DriverProfile.tsx index ccca3c975..f1d63e0f8 100644 --- a/apps/website/components/alpha/DriverProfile.tsx +++ b/apps/website/components/drivers/DriverProfile.tsx @@ -1,13 +1,13 @@ 'use client'; -import { DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers'; +import { DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers'; import Card from '../ui/Card'; -import ProfileHeader from './ProfileHeader'; +import ProfileHeader from '../profile/ProfileHeader'; import ProfileStats from './ProfileStats'; import CareerHighlights from './CareerHighlights'; import DriverRankings from './DriverRankings'; import PerformanceMetrics from './PerformanceMetrics'; -import { getDriverTeam } from '@/lib/team-data'; +import { getDriverTeam } from '@gridpilot/racing/application'; import { getDriverStats, getLeagueRankings } from '@/lib/di-container'; interface DriverProfileProps { diff --git a/apps/website/components/drivers/DriverRankings.tsx b/apps/website/components/drivers/DriverRankings.tsx new file mode 100644 index 000000000..db334f106 --- /dev/null +++ b/apps/website/components/drivers/DriverRankings.tsx @@ -0,0 +1,81 @@ +import Card from '@/components/ui/Card'; + +export interface DriverRanking { + type: 'overall' | 'league'; + name: string; + rank: number; + totalDrivers: number; + percentile: number; + rating: number; +} + +interface DriverRankingsProps { + rankings: DriverRanking[]; +} + +export default function DriverRankings({ rankings }: DriverRankingsProps) { + if (!rankings || rankings.length === 0) { + return ( + +
+

Rankings

+

+ No ranking data available yet. Compete in leagues to earn your first results. +

+
+
+ ); + } + + return ( + +
+

Rankings

+
+ {rankings.map((ranking, index) => ( +
+
+ + {ranking.name} + + + {ranking.type === 'overall' ? 'Overall' : 'League'} ranking + +
+ +
+
+
+ #{ranking.rank} +
+
Position
+
+
+
+ {ranking.totalDrivers} +
+
Drivers
+
+
+
+ {ranking.percentile.toFixed(1)}% +
+
Percentile
+
+
+
+ {ranking.rating} +
+
Rating
+
+
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/website/components/alpha/PerformanceMetrics.tsx b/apps/website/components/drivers/PerformanceMetrics.tsx similarity index 100% rename from apps/website/components/alpha/PerformanceMetrics.tsx rename to apps/website/components/drivers/PerformanceMetrics.tsx diff --git a/apps/website/components/alpha/ProfileRaceHistory.tsx b/apps/website/components/drivers/ProfileRaceHistory.tsx similarity index 100% rename from apps/website/components/alpha/ProfileRaceHistory.tsx rename to apps/website/components/drivers/ProfileRaceHistory.tsx diff --git a/apps/website/components/alpha/ProfileSettings.tsx b/apps/website/components/drivers/ProfileSettings.tsx similarity index 99% rename from apps/website/components/alpha/ProfileSettings.tsx rename to apps/website/components/drivers/ProfileSettings.tsx index bdfd3e519..1a153a47a 100644 --- a/apps/website/components/alpha/ProfileSettings.tsx +++ b/apps/website/components/drivers/ProfileSettings.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers'; +import { DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers'; import Card from '../ui/Card'; import Button from '../ui/Button'; import Input from '../ui/Input'; diff --git a/apps/website/components/alpha/ProfileStats.tsx b/apps/website/components/drivers/ProfileStats.tsx similarity index 100% rename from apps/website/components/alpha/ProfileStats.tsx rename to apps/website/components/drivers/ProfileStats.tsx diff --git a/apps/website/components/alpha/RankBadge.tsx b/apps/website/components/drivers/RankBadge.tsx similarity index 100% rename from apps/website/components/alpha/RankBadge.tsx rename to apps/website/components/drivers/RankBadge.tsx diff --git a/apps/website/components/alpha/RatingBreakdown.tsx b/apps/website/components/drivers/RatingBreakdown.tsx similarity index 100% rename from apps/website/components/alpha/RatingBreakdown.tsx rename to apps/website/components/drivers/RatingBreakdown.tsx diff --git a/apps/website/components/feed/FeedEmptyState.tsx b/apps/website/components/feed/FeedEmptyState.tsx new file mode 100644 index 000000000..696884959 --- /dev/null +++ b/apps/website/components/feed/FeedEmptyState.tsx @@ -0,0 +1,25 @@ +import Card from '@/components/ui/Card'; +import Button from '@/components/ui/Button'; + +export default function FeedEmptyState() { + return ( + +
🏁
+

+ Your feed is warming up +

+

+ As leagues, teams, and friends start racing, this feed will show their latest results, + signups, and highlights. +

+ +
+ ); +} \ No newline at end of file diff --git a/apps/website/components/feed/FeedItemCard.tsx b/apps/website/components/feed/FeedItemCard.tsx new file mode 100644 index 000000000..37159c804 --- /dev/null +++ b/apps/website/components/feed/FeedItemCard.tsx @@ -0,0 +1,83 @@ +import Card from '@/components/ui/Card'; +import Button from '@/components/ui/Button'; +import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem'; +import { friends } from '@gridpilot/testing-support'; + +function timeAgo(timestamp: Date): string { + const diffMs = Date.now() - timestamp.getTime(); + const diffMinutes = Math.floor(diffMs / 60000); + if (diffMinutes < 1) return 'Just now'; + if (diffMinutes < 60) return `${diffMinutes} min ago`; + const diffHours = Math.floor(diffMinutes / 60); + if (diffHours < 24) return `${diffHours} h ago`; + const diffDays = Math.floor(diffHours / 24); + return `${diffDays} d ago`; +} + +function getActor(item: FeedItem) { + if (item.actorFriendId) { + const friend = friends.find(f => f.driverId === item.actorFriendId); + if (friend) { + return { + name: friend.displayName, + avatarUrl: friend.avatarUrl + }; + } + } + return null; +} + +interface FeedItemCardProps { + item: FeedItem; +} + +export default function FeedItemCard({ item }: FeedItemCardProps) { + const actor = getActor(item); + + return ( +
+
+ {actor ? ( +
+ {actor.name} +
+ ) : ( + + + {item.type.startsWith('friend') ? 'FR' : 'LG'} + + + )} +
+
+
+
+

{item.headline}

+ {item.body && ( +

{item.body}

+ )} +
+ + {timeAgo(item.timestamp)} + +
+ {(item.ctaHref && item.ctaLabel) && ( +
+ +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/website/components/feed/FeedLayout.tsx b/apps/website/components/feed/FeedLayout.tsx new file mode 100644 index 000000000..b3666a1a0 --- /dev/null +++ b/apps/website/components/feed/FeedLayout.tsx @@ -0,0 +1,43 @@ +import Card from '@/components/ui/Card'; +import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem'; +import type { Race } from '@gridpilot/racing/domain/entities/Race'; +import type { RaceWithResultsDTO } from '@gridpilot/testing-support'; +import FeedList from '@/components/feed/FeedList'; +import UpcomingRacesSidebar from '@/components/races/UpcomingRacesSidebar'; +import LatestResultsSidebar from '@/components/races/LatestResultsSidebar'; + +interface FeedLayoutProps { + feedItems: FeedItem[]; + upcomingRaces: Race[]; + latestResults: RaceWithResultsDTO[]; +} + +export default function FeedLayout({ + feedItems, + upcomingRaces, + latestResults +}: FeedLayoutProps) { + return ( +
+
+
+
+
+

Activity

+

+ See what your friends and leagues are doing right now. +

+
+
+ + + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/website/components/feed/FeedList.tsx b/apps/website/components/feed/FeedList.tsx new file mode 100644 index 000000000..94d8fa7ef --- /dev/null +++ b/apps/website/components/feed/FeedList.tsx @@ -0,0 +1,21 @@ +import FeedEmptyState from '@/components/feed/FeedEmptyState'; +import FeedItemCard from '@/components/feed/FeedItemCard'; +import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem'; + +interface FeedListProps { + items: FeedItem[]; +} + +export default function FeedList({ items }: FeedListProps) { + if (!items.length) { + return ; + } + + return ( +
+ {items.map(item => ( + + ))} +
+ ); +} \ No newline at end of file diff --git a/apps/website/components/layout/Breadcrumbs.tsx b/apps/website/components/layout/Breadcrumbs.tsx new file mode 100644 index 000000000..cb73f1499 --- /dev/null +++ b/apps/website/components/layout/Breadcrumbs.tsx @@ -0,0 +1,53 @@ +'use client'; + +import Link from 'next/link'; + +export type BreadcrumbItem = { + label: string; + href?: string; +}; + +interface BreadcrumbsProps { + items: BreadcrumbItem[]; + className?: string; +} + +export default function Breadcrumbs({ items, className }: BreadcrumbsProps) { + if (!items || items.length === 0) { + return null; + } + + const lastIndex = items.length - 1; + + return ( + + ); +} \ No newline at end of file diff --git a/apps/website/components/alpha/CreateLeagueForm.tsx b/apps/website/components/leagues/CreateLeagueForm.tsx similarity index 97% rename from apps/website/components/alpha/CreateLeagueForm.tsx rename to apps/website/components/leagues/CreateLeagueForm.tsx index f3ef85fcf..1932c44cd 100644 --- a/apps/website/components/alpha/CreateLeagueForm.tsx +++ b/apps/website/components/leagues/CreateLeagueForm.tsx @@ -4,8 +4,7 @@ import { useState, FormEvent } from 'react'; import { useRouter } from 'next/navigation'; import Input from '../ui/Input'; import Button from '../ui/Button'; -import DataWarning from './DataWarning'; -import { League } from '@gridpilot/racing-domain/entities/League'; +import { League } from '@gridpilot/racing/domain/entities/League'; import { getLeagueRepository, getDriverRepository } from '../../lib/di-container'; interface FormErrors { @@ -98,7 +97,6 @@ export default function CreateLeagueForm() { return ( <> -