From fb509607c11b8baffeb35729a5f3111fba062bc2 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 4 Dec 2025 23:31:55 +0100 Subject: [PATCH] wip --- apps/website/app/drivers/[id]/page.tsx | 40 +- apps/website/app/drivers/page.tsx | 259 ++++---- apps/website/app/layout.tsx | 16 +- apps/website/app/leagues/[id]/page.tsx | 272 +++++++-- .../app/leagues/[id]/races/[raceId]/page.tsx | 53 +- .../app/leagues/[id]/standings/page.tsx | 21 +- apps/website/app/leagues/page.tsx | 10 +- apps/website/app/profile/leagues/page.tsx | 199 +++++++ apps/website/app/profile/page.tsx | 185 +----- apps/website/app/races/[id]/page.tsx | 56 +- apps/website/app/races/page.tsx | 49 -- apps/website/app/teams/[id]/page.tsx | 109 ++-- apps/website/app/teams/page.tsx | 163 ++++- apps/website/components/alpha/AlphaNav.tsx | 31 +- .../website/components/drivers/DriverCard.tsx | 34 +- .../components/drivers/DriverIdentity.tsx | 63 ++ .../components/drivers/DriverProfile.tsx | 77 ++- .../components/drivers/ProfileStats.tsx | 128 ++-- .../components/leagues/JoinLeagueButton.tsx | 46 +- .../components/leagues/LeagueAdmin.tsx | 540 +++++++++++++---- .../website/components/leagues/LeagueCard.tsx | 62 +- .../components/leagues/LeagueHeader.tsx | 118 +++- .../components/leagues/LeagueMembers.tsx | 61 +- .../components/leagues/LeagueSchedule.tsx | 52 +- .../components/leagues/MembershipStatus.tsx | 5 +- .../components/leagues/StandingsTable.tsx | 132 ++++- .../components/profile/DriverRatingPill.tsx | 28 + .../components/profile/DriverSummaryPill.tsx | 74 +++ .../components/profile/ProfileHeader.tsx | 63 +- apps/website/components/profile/UserPill.tsx | 171 ++++++ .../components/teams/CreateTeamForm.tsx | 15 +- .../components/teams/JoinTeamButton.tsx | 65 +- apps/website/components/teams/TeamAdmin.tsx | 41 +- apps/website/components/teams/TeamCard.tsx | 36 +- .../components/teams/TeamLadderRow.tsx | 78 +++ apps/website/components/teams/TeamRoster.tsx | 130 ++-- .../components/teams/TeamStandings.tsx | 6 +- apps/website/components/ui/Modal.tsx | 182 ++++++ apps/website/lib/currentDriver.ts | 20 + apps/website/lib/di-container.ts | 484 +++++++++++++++ apps/website/lib/leagueMembership.ts | 89 +++ apps/website/lib/leagueRoles.ts | 2 +- apps/website/lib/racingLegacyFacade.ts | 558 ------------------ apps/website/package.json | 3 +- apps/website/tsconfig.json | 4 +- package-lock.json | 19 +- packages/demo-infrastructure/index.ts | 1 + .../media/DemoImageServiceAdapter.ts | 31 + packages/demo-infrastructure/package.json | 15 + packages/demo-infrastructure/tsconfig.json | 13 + .../application/ports/ImageServicePort.ts | 6 + packages/media/index.ts | 1 + packages/media/package.json | 11 + packages/media/tsconfig.json | 10 + .../dto/ChampionshipStandingsDTO.ts | 16 + packages/racing/application/dto/LeagueDTO.ts | 11 + .../dto/LeagueDriverSeasonStatsDTO.ts | 20 + packages/racing/application/index.ts | 11 +- .../application/mappers/EntityMappers.ts | 17 + .../GetAllLeaguesWithCapacityQuery.ts | 58 ++ .../GetLeagueDriverSeasonStatsQuery.ts | 97 +++ .../use-cases/GetLeagueStandingsQuery.ts | 18 + ...RecalculateChampionshipStandingsUseCase.ts | 132 +++++ .../domain/entities/ChampionshipStanding.ts | 41 ++ packages/racing/domain/entities/Game.ts | 24 + packages/racing/domain/entities/League.ts | 19 + .../domain/entities/LeagueScoringConfig.ts | 7 + packages/racing/domain/entities/Penalty.ts | 29 + packages/racing/domain/entities/Season.ts | 77 +++ .../IChampionshipStandingRepository.ts | 10 + .../domain/repositories/IGameRepository.ts | 6 + .../ILeagueScoringConfigRepository.ts | 5 + .../domain/repositories/IPenaltyRepository.ts | 25 + .../domain/repositories/ISeasonRepository.ts | 6 + .../domain/services/ChampionshipAggregator.ts | 71 +++ .../domain/services/DropScoreApplier.ts | 56 ++ .../domain/services/EventScoringService.ts | 128 ++++ .../racing/domain/value-objects/BonusRule.ts | 8 + .../value-objects/ChampionshipConfig.ts | 15 + .../domain/value-objects/ChampionshipType.ts | 1 + .../domain/value-objects/DropScorePolicy.ts | 13 + .../domain/value-objects/ParticipantRef.ts | 6 + .../domain/value-objects/PointsTable.ts | 21 + .../domain/value-objects/SessionType.ts | 9 + .../InMemoryLeagueMembershipRepository.ts | 100 ++++ .../repositories/InMemoryPenaltyRepository.ts | 85 +++ .../InMemoryRaceRegistrationRepository.ts | 115 ++++ .../InMemoryScoringRepositories.ts | 229 +++++++ .../InMemoryTeamMembershipRepository.ts | 135 +++++ .../repositories/InMemoryTeamRepository.ts | 67 +++ packages/testing-support/src/images/images.ts | 18 + .../src/racing/StaticRacingSeed.ts | 21 + ...culateChampionshipStandingsUseCase.test.ts | 426 +++++++++++++ .../domain/services/DropScoreApplier.test.ts | 88 +++ .../services/EventScoringService.test.ts | 266 +++++++++ tsconfig.json | 4 +- 96 files changed, 5839 insertions(+), 1609 deletions(-) create mode 100644 apps/website/app/profile/leagues/page.tsx create mode 100644 apps/website/components/drivers/DriverIdentity.tsx create mode 100644 apps/website/components/profile/DriverRatingPill.tsx create mode 100644 apps/website/components/profile/DriverSummaryPill.tsx create mode 100644 apps/website/components/profile/UserPill.tsx create mode 100644 apps/website/components/teams/TeamLadderRow.tsx create mode 100644 apps/website/components/ui/Modal.tsx create mode 100644 apps/website/lib/currentDriver.ts create mode 100644 apps/website/lib/leagueMembership.ts delete mode 100644 apps/website/lib/racingLegacyFacade.ts create mode 100644 packages/demo-infrastructure/index.ts create mode 100644 packages/demo-infrastructure/media/DemoImageServiceAdapter.ts create mode 100644 packages/demo-infrastructure/package.json create mode 100644 packages/demo-infrastructure/tsconfig.json create mode 100644 packages/media/application/ports/ImageServicePort.ts create mode 100644 packages/media/index.ts create mode 100644 packages/media/package.json create mode 100644 packages/media/tsconfig.json create mode 100644 packages/racing/application/dto/ChampionshipStandingsDTO.ts create mode 100644 packages/racing/application/dto/LeagueDriverSeasonStatsDTO.ts create mode 100644 packages/racing/application/use-cases/GetAllLeaguesWithCapacityQuery.ts create mode 100644 packages/racing/application/use-cases/GetLeagueDriverSeasonStatsQuery.ts create mode 100644 packages/racing/application/use-cases/GetLeagueStandingsQuery.ts create mode 100644 packages/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts create mode 100644 packages/racing/domain/entities/ChampionshipStanding.ts create mode 100644 packages/racing/domain/entities/Game.ts create mode 100644 packages/racing/domain/entities/LeagueScoringConfig.ts create mode 100644 packages/racing/domain/entities/Penalty.ts create mode 100644 packages/racing/domain/entities/Season.ts create mode 100644 packages/racing/domain/repositories/IChampionshipStandingRepository.ts create mode 100644 packages/racing/domain/repositories/IGameRepository.ts create mode 100644 packages/racing/domain/repositories/ILeagueScoringConfigRepository.ts create mode 100644 packages/racing/domain/repositories/IPenaltyRepository.ts create mode 100644 packages/racing/domain/repositories/ISeasonRepository.ts create mode 100644 packages/racing/domain/services/ChampionshipAggregator.ts create mode 100644 packages/racing/domain/services/DropScoreApplier.ts create mode 100644 packages/racing/domain/services/EventScoringService.ts create mode 100644 packages/racing/domain/value-objects/BonusRule.ts create mode 100644 packages/racing/domain/value-objects/ChampionshipConfig.ts create mode 100644 packages/racing/domain/value-objects/ChampionshipType.ts create mode 100644 packages/racing/domain/value-objects/DropScorePolicy.ts create mode 100644 packages/racing/domain/value-objects/ParticipantRef.ts create mode 100644 packages/racing/domain/value-objects/PointsTable.ts create mode 100644 packages/racing/domain/value-objects/SessionType.ts create mode 100644 packages/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository.ts create mode 100644 packages/racing/infrastructure/repositories/InMemoryPenaltyRepository.ts create mode 100644 packages/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository.ts create mode 100644 packages/racing/infrastructure/repositories/InMemoryScoringRepositories.ts create mode 100644 packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts create mode 100644 packages/racing/infrastructure/repositories/InMemoryTeamRepository.ts create mode 100644 tests/unit/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts create mode 100644 tests/unit/domain/services/DropScoreApplier.test.ts create mode 100644 tests/unit/domain/services/EventScoringService.test.ts diff --git a/apps/website/app/drivers/[id]/page.tsx b/apps/website/app/drivers/[id]/page.tsx index 4dc85d13b..f3dae6479 100644 --- a/apps/website/app/drivers/[id]/page.tsx +++ b/apps/website/app/drivers/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, use } from 'react'; import Link from 'next/link'; import { useRouter, useParams } from 'next/navigation'; import { getDriverRepository } from '@/lib/di-container'; @@ -14,7 +14,7 @@ import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO'; export default function DriverDetailPage({ searchParams, }: { - searchParams?: { [key: string]: string | string[] | undefined }; + searchParams: any; }) { const router = useRouter(); const params = useParams(); @@ -24,14 +24,36 @@ export default function DriverDetailPage({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const unwrappedSearchParams = use(searchParams) as URLSearchParams | undefined; + const from = - typeof searchParams?.from === 'string' ? searchParams.from : undefined; - const leagueId = - typeof searchParams?.leagueId === 'string' - ? searchParams.leagueId + typeof unwrappedSearchParams?.get === 'function' + ? unwrappedSearchParams.get('from') ?? undefined : undefined; - const backLink = - from === 'league' && leagueId ? `/leagues/${leagueId}` : null; + + const leagueId = + typeof unwrappedSearchParams?.get === 'function' + ? unwrappedSearchParams.get('leagueId') ?? undefined + : undefined; + + const raceId = + typeof unwrappedSearchParams?.get === 'function' + ? unwrappedSearchParams.get('raceId') ?? undefined + : undefined; + + let backLink: string | null = null; + + if (from === 'league-standings' && leagueId) { + backLink = `/leagues/${leagueId}/standings`; + } else if (from === 'league' && leagueId) { + backLink = `/leagues/${leagueId}`; + } else if (from === 'league-members' && leagueId) { + backLink = `/leagues/${leagueId}`; + } else if (from === 'league-race' && leagueId && raceId) { + backLink = `/leagues/${leagueId}/races/${raceId}`; + } else { + backLink = null; + } useEffect(() => { loadDriver(); @@ -119,7 +141,7 @@ export default function DriverDetailPage({ /> {/* Driver Profile Component */} - + ); diff --git a/apps/website/app/drivers/page.tsx b/apps/website/app/drivers/page.tsx index 285d134c6..1796ef595 100644 --- a/apps/website/app/drivers/page.tsx +++ b/apps/website/app/drivers/page.tsx @@ -1,127 +1,101 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import Image from 'next/image'; import DriverCard from '@/components/drivers/DriverCard'; import RankBadge from '@/components/drivers/RankBadge'; import Input from '@/components/ui/Input'; import Card from '@/components/ui/Card'; -import { getDriverAvatarUrl } from '@/lib/racingLegacyFacade'; +import { getDriverRepository, getDriverStats, getAllDriverRankings } from '@/lib/di-container'; -// Mock data (fictional demo drivers only) -const MOCK_DRIVERS = [ - { - id: 'driver-1', - name: 'Alex Vermeer', - rating: 3245, - skillLevel: 'pro' as const, - nationality: 'Netherlands', - racesCompleted: 156, - wins: 45, - podiums: 89, - isActive: true, - rank: 1, - }, - { - id: 'driver-2', - name: 'Liam Hartmann', - rating: 3198, - skillLevel: 'pro' as const, - nationality: 'United Kingdom', - racesCompleted: 234, - wins: 78, - podiums: 145, - isActive: true, - rank: 2, - }, - { - id: 'driver-3', - name: 'Michael Schmidt', - rating: 2912, - skillLevel: 'advanced' as const, - nationality: 'Germany', - racesCompleted: 145, - wins: 34, - podiums: 67, - isActive: true, - rank: 3, - }, - { - id: 'driver-4', - name: 'Emma Thompson', - rating: 2789, - skillLevel: 'advanced' as const, - nationality: 'Australia', - racesCompleted: 112, - wins: 23, - podiums: 56, - isActive: true, - rank: 5, - }, - { - id: 'driver-5', - name: 'Sarah Chen', - rating: 2456, - skillLevel: 'advanced' as const, - nationality: 'Singapore', - racesCompleted: 89, - wins: 12, - podiums: 34, - isActive: true, - rank: 8, - }, - { - id: 'driver-6', - name: 'Isabella Rossi', - rating: 2145, - skillLevel: 'intermediate' as const, - nationality: 'Italy', - racesCompleted: 67, - wins: 8, - podiums: 23, - isActive: true, - rank: 12, - }, - { - id: 'driver-7', - name: 'Carlos Rodriguez', - rating: 1876, - skillLevel: 'intermediate' as const, - nationality: 'Spain', - racesCompleted: 45, - wins: 3, - podiums: 12, - isActive: false, - rank: 18, - }, - { - id: 'driver-8', - name: 'Yuki Tanaka', - rating: 1234, - skillLevel: 'beginner' as const, - nationality: 'Japan', - racesCompleted: 12, - wins: 0, - podiums: 2, - isActive: true, - rank: 45, - }, -]; +type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; + +type DriverListItem = { + id: string; + name: string; + rating: number; + skillLevel: SkillLevel; + nationality: string; + racesCompleted: number; + wins: number; + podiums: number; + isActive: boolean; + rank: number; +}; export default function DriversPage() { const router = useRouter(); + const [drivers, setDrivers] = useState([]); + const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); - const [selectedSkill, setSelectedSkill] = useState('all'); + const [selectedSkill, setSelectedSkill] = useState<'all' | SkillLevel>('all'); const [selectedNationality, setSelectedNationality] = useState('all'); const [activeOnly, setActiveOnly] = useState(false); const [sortBy, setSortBy] = useState<'rank' | 'rating' | 'wins' | 'podiums'>('rank'); + useEffect(() => { + const load = async () => { + const driverRepo = getDriverRepository(); + const allDrivers = await driverRepo.findAll(); + const rankings = getAllDriverRankings(); + + const items: DriverListItem[] = allDrivers.map((driver) => { + const stats = getDriverStats(driver.id); + const rating = stats?.rating ?? 0; + const wins = stats?.wins ?? 0; + const podiums = stats?.podiums ?? 0; + const totalRaces = stats?.totalRaces ?? 0; + + let effectiveRank = Number.POSITIVE_INFINITY; + + if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) { + effectiveRank = stats.overallRank; + } else { + const indexInGlobal = rankings.findIndex( + (entry) => entry.driverId === driver.id, + ); + if (indexInGlobal !== -1) { + effectiveRank = indexInGlobal + 1; + } + } + + const skillLevel: SkillLevel = + rating >= 3000 + ? 'pro' + : rating >= 2500 + ? 'advanced' + : rating >= 1800 + ? 'intermediate' + : 'beginner'; + + const isActive = rankings.some((r) => r.driverId === driver.id); + + return { + id: driver.id, + name: driver.name, + rating, + skillLevel, + nationality: driver.country, + racesCompleted: totalRaces, + wins, + podiums, + isActive, + rank: effectiveRank, + }; + }); + + setDrivers(items); + setLoading(false); + }; + + void load(); + }, []); + const nationalities = Array.from( - new Set(MOCK_DRIVERS.map((d) => d.nationality).filter(Boolean)) + new Set(drivers.map((d) => d.nationality).filter(Boolean)), ).sort(); - const filteredDrivers = MOCK_DRIVERS.filter((driver) => { + const filteredDrivers = drivers.filter((driver) => { const matchesSearch = driver.name .toLowerCase() .includes(searchQuery.toLowerCase()); @@ -135,9 +109,12 @@ export default function DriversPage() { }); const sortedDrivers = [...filteredDrivers].sort((a, b) => { + const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY; + const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY; + switch (sortBy) { case 'rank': - return a.rank - b.rank; + return rankA - rankB || b.rating - a.rating || a.name.localeCompare(b.name); case 'rating': return b.rating - a.rating; case 'wins': @@ -153,6 +130,14 @@ export default function DriversPage() { router.push(`/drivers/${driverId}`); }; + if (loading) { + return ( +
+
Loading drivers...
+
+ ); + } + return (
@@ -183,7 +168,7 @@ export default function DriversPage() {