From 6a88fe93ab76c835b2c97d15699bb677203325d3 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 12 Dec 2025 01:11:36 +0100 Subject: [PATCH] wip --- apps/companion/main/di-config.ts | 13 +- apps/companion/main/ipc-handlers.ts | 9 +- apps/companion/main/preload.ts | 17 ++- apps/companion/renderer/App.tsx | 20 ++- .../app/api/sponsors/dashboard/route.ts | 2 +- .../app/api/sponsors/sponsorships/route.ts | 2 +- apps/website/app/auth/login/page.tsx | 6 +- apps/website/app/auth/signup/page.tsx | 10 +- apps/website/app/dashboard/page.tsx | 6 +- apps/website/app/drivers/page.tsx | 16 ++- .../website/app/leaderboards/drivers/page.tsx | 10 +- apps/website/app/leaderboards/page.tsx | 8 +- apps/website/app/leagues/[id]/page.tsx | 6 +- .../app/leagues/[id]/rulebook/page.tsx | 6 +- .../app/leagues/[id]/standings/page.tsx | 13 +- apps/website/app/leagues/page.tsx | 21 ++- apps/website/app/races/[id]/page.tsx | 6 +- apps/website/app/races/[id]/results/page.tsx | 16 ++- apps/website/app/races/all/page.tsx | 6 +- apps/website/app/races/page.tsx | 6 +- apps/website/app/teams/[id]/page.tsx | 17 +-- .../website/components/leagues/LeagueCard.tsx | 6 +- .../mockups/DriverProfileMockup.tsx | 4 +- .../mockups/RatingFactorsMockup.tsx | 4 +- apps/website/env.d.ts | 49 +++++++ apps/website/lib/di-config.ts | 46 ++----- apps/website/lib/di-container.ts | 2 +- ...lLeaguesWithCapacityAndScoringPresenter.ts | 16 +-- .../AllLeaguesWithCapacityPresenter.ts | 19 ++- .../lib/presenters/AllRacesPagePresenter.ts | 9 +- .../presenters/DashboardOverviewPresenter.ts | 9 +- .../presenters/DriversLeaderboardPresenter.ts | 22 ++- .../LeagueDriverSeasonStatsPresenter.ts | 22 ++- .../LeagueScoringConfigPresenter.ts | 4 + .../lib/presenters/RaceDetailPresenter.ts | 6 +- .../presenters/RaceRegistrationsPresenter.ts | 15 +- .../presenters/RaceResultsDetailPresenter.ts | 7 +- .../lib/presenters/RaceWithSOFPresenter.ts | 52 +++---- .../lib/presenters/RacesPagePresenter.ts | 22 +-- .../presenters/SponsorDashboardPresenter.ts | 19 ++- .../SponsorSponsorshipsPresenter.ts | 19 ++- .../lib/presenters/TeamDetailsPresenter.ts | 22 ++- apps/website/tsconfig.json | 28 ++-- apps/website/types/third-party-shims.d.ts | 4 + .../engine/AutomationEngineAdapter.ts | 33 ++++- .../engine/MockAutomationEngineAdapter.ts | 33 ++++- packages/automation/tsconfig.json | 2 +- packages/identity/tsconfig.json | 2 +- packages/media/tsconfig.json | 2 +- ...lLeaguesWithCapacityAndScoringPresenter.ts | 10 +- .../IAllLeaguesWithCapacityPresenter.ts | 14 +- .../presenters/IAllRacesPagePresenter.ts | 10 +- .../presenters/IAllTeamsPresenter.ts | 12 +- .../presenters/IDashboardOverviewPresenter.ts | 10 +- .../IDriversLeaderboardPresenter.ts | 27 ++-- .../ILeagueDriverSeasonStatsPresenter.ts | 32 +++-- .../ILeagueScoringConfigPresenter.ts | 7 +- .../presenters/IRaceDetailPresenter.ts | 7 +- .../presenters/IRacePenaltiesPresenter.ts | 18 +-- .../presenters/IRaceProtestsPresenter.ts | 17 +-- .../presenters/IRaceRegistrationsPresenter.ts | 12 +- .../presenters/IRaceResultsDetailPresenter.ts | 9 +- .../presenters/IRaceWithSOFPresenter.ts | 38 +++--- .../presenters/IRacesPagePresenter.ts | 12 +- .../presenters/ISponsorDashboardPresenter.ts | 10 +- .../ISponsorSponsorshipsPresenter.ts | 8 +- .../presenters/ITeamDetailsPresenter.ts | 20 +-- .../use-cases/CreateTeamUseCase.ts | 5 +- ...AllLeaguesWithCapacityAndScoringUseCase.ts | 48 +++++-- .../GetAllLeaguesWithCapacityUseCase.ts | 25 +++- .../use-cases/GetAllRacesPageDataUseCase.ts | 11 +- .../use-cases/GetAllTeamsUseCase.ts | 10 +- .../use-cases/GetDashboardOverviewUseCase.ts | 10 +- .../use-cases/GetDriversLeaderboardUseCase.ts | 35 +++-- .../GetLeagueDriverSeasonStatsUseCase.ts | 46 +++++-- .../use-cases/GetLeagueFullConfigUseCase.ts | 6 +- .../GetLeagueScoringConfigUseCase.ts | 17 ++- .../use-cases/GetRaceDetailUseCase.ts | 26 ++-- .../use-cases/GetRaceRegistrationsUseCase.ts | 26 +++- .../use-cases/GetRaceResultsDetailUseCase.ts | 53 ++++--- .../use-cases/GetRaceWithSOFUseCase.ts | 50 ++++--- .../use-cases/GetRacesPageDataUseCase.ts | 22 ++- .../use-cases/GetSponsorDashboardUseCase.ts | 28 ++-- .../GetSponsorSponsorshipsUseCase.ts | 26 +++- .../use-cases/GetTeamDetailsUseCase.ts | 28 +++- .../use-cases/PreviewLeagueScheduleUseCase.ts | 11 +- .../use-cases/UpdateTeamUseCase.ts | 10 +- packages/racing/domain/entities/Car.ts | 6 +- packages/racing/domain/entities/Driver.ts | 14 +- packages/racing/domain/entities/League.ts | 14 +- packages/racing/domain/entities/Penalty.ts | 10 +- packages/racing/domain/entities/Protest.ts | 14 +- packages/racing/domain/entities/Race.ts | 129 +++++++++++++++--- packages/racing/domain/entities/Season.ts | 8 +- .../domain/entities/SeasonSponsorship.ts | 45 ++++-- packages/racing/domain/entities/Sponsor.ts | 48 +++++-- .../domain/entities/SponsorshipRequest.ts | 80 +++++++++-- packages/racing/domain/entities/Standing.ts | 13 +- packages/racing/domain/entities/Track.ts | 14 +- .../repositories/ITeamMembershipRepository.ts | 5 + .../domain/services/EventScoringService.ts | 4 + .../domain/services/IDriverStatsService.ts | 13 ++ .../racing/domain/services/IRankingService.ts | 11 ++ .../services/SeasonScheduleGenerator.ts | 4 +- .../domain/value-objects/GameConstraints.ts | 13 +- .../value-objects/SponsorshipPricing.ts | 24 ++-- .../repositories/InMemoryResultRepository.ts | 6 +- .../InMemoryStandingRepository.ts | 31 +++-- .../InMemoryTeamMembershipRepository.ts | 5 + packages/racing/tsconfig.json | 2 +- packages/shared/domain/Option.ts | 7 + packages/shared/domain/index.ts | 3 +- packages/social/tsconfig.json | 2 +- packages/testing-support/src/images/images.ts | 9 +- .../src/media/DemoAvatarGenerationAdapter.ts | 10 +- .../src/media/DemoImageServiceAdapter.ts | 3 +- .../InMemoryAvatarGenerationRepository.ts | 9 +- .../src/racing/RacingFeedSeed.ts | 64 +++++++-- .../src/racing/RacingSeedCore.ts | 55 +++++--- .../src/racing/RacingSponsorshipSeed.ts | 74 +++++----- packages/testing-support/tsconfig.json | 2 +- tsconfig.base.json | 56 ++++++++ tsconfig.electron.json | 4 +- tsconfig.json | 43 +----- tsconfig.tests.json | 2 +- 125 files changed, 1513 insertions(+), 803 deletions(-) create mode 100644 apps/website/types/third-party-shims.d.ts create mode 100644 packages/racing/domain/services/IDriverStatsService.ts create mode 100644 packages/racing/domain/services/IRankingService.ts create mode 100644 packages/shared/domain/Option.ts create mode 100644 tsconfig.base.json diff --git a/apps/companion/main/di-config.ts b/apps/companion/main/di-config.ts index 4359d37cf..5e32f2695 100644 --- a/apps/companion/main/di-config.ts +++ b/apps/companion/main/di-config.ts @@ -255,7 +255,18 @@ export function configureDIContainer(): void { } // Overlay Sync Service - create singleton instance directly - const lifecycleEmitter = browserAutomation as unknown as IAutomationLifecycleEmitter; + const lifecycleEmitter: IAutomationLifecycleEmitter = { + onLifecycle: (cb) => { + if ('onLifecycle' in browserAutomation && typeof (browserAutomation as { onLifecycle?: unknown }).onLifecycle === 'function') { + (browserAutomation as IAutomationLifecycleEmitter).onLifecycle(cb); + } + }, + offLifecycle: (cb) => { + if ('offLifecycle' in browserAutomation && typeof (browserAutomation as { offLifecycle?: unknown }).offLifecycle === 'function') { + (browserAutomation as IAutomationLifecycleEmitter).offLifecycle(cb); + } + }, + }; const publisher = { publish: async (event: unknown) => { try { diff --git a/apps/companion/main/ipc-handlers.ts b/apps/companion/main/ipc-handlers.ts index 88f88ac19..c8f593dd8 100644 --- a/apps/companion/main/ipc-handlers.ts +++ b/apps/companion/main/ipc-handlers.ts @@ -374,9 +374,10 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void { // Subscribe to automation adapter lifecycle events and relay to renderer try { if (!lifecycleSubscribed) { - const lifecycleEmitter = container.getBrowserAutomation() as unknown as IAutomationLifecycleEmitter; - if (typeof lifecycleEmitter.onLifecycle === 'function') { - lifecycleEmitter.onLifecycle((ev) => { + const browserAutomation = container.getBrowserAutomation(); + const candidate = browserAutomation as Partial; + if (typeof candidate.onLifecycle === 'function' && typeof candidate.offLifecycle === 'function') { + candidate.onLifecycle((ev) => { try { if (mainWindow && mainWindow.webContents) { mainWindow.webContents.send('automation-event', ev); @@ -388,6 +389,8 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void { }); lifecycleSubscribed = true; logger.debug('Subscribed to adapter lifecycle events for renderer relay'); + } else { + logger.debug?.('Browser automation does not expose lifecycle events; skipping subscription'); } } } catch (e) { diff --git a/apps/companion/main/preload.ts b/apps/companion/main/preload.ts index 8a40c6f13..78e16a9ce 100644 --- a/apps/companion/main/preload.ts +++ b/apps/companion/main/preload.ts @@ -31,12 +31,16 @@ export interface CheckoutConfirmationRequest { timeoutMs: number; } +export interface StartAutomationResponse { + success: boolean; + sessionId?: string; + error?: string; + authRequired?: boolean; + authState?: AuthenticationState; +} + export interface ElectronAPI { - startAutomation: (config: HostedSessionConfig) => Promise<{ - success: boolean; - sessionId?: string; - error?: string; - }>; + startAutomation: (config: HostedSessionConfig) => Promise; stopAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>; getSessionStatus: (sessionId: string) => Promise; pauseAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>; @@ -60,7 +64,8 @@ export interface ElectronAPI { } contextBridge.exposeInMainWorld('electronAPI', { - startAutomation: (config: HostedSessionConfig) => ipcRenderer.invoke('start-automation', config), + startAutomation: (config: HostedSessionConfig) => + ipcRenderer.invoke('start-automation', config) as Promise, stopAutomation: (sessionId: string) => ipcRenderer.invoke('stop-automation', sessionId), getSessionStatus: (sessionId: string) => ipcRenderer.invoke('get-session-status', sessionId), pauseAutomation: (sessionId: string) => ipcRenderer.invoke('pause-automation', sessionId), diff --git a/apps/companion/renderer/App.tsx b/apps/companion/renderer/App.tsx index 880ba26fb..253b045fb 100644 --- a/apps/companion/renderer/App.tsx +++ b/apps/companion/renderer/App.tsx @@ -6,6 +6,8 @@ import { BrowserModeToggle } from './components/BrowserModeToggle'; import { CheckoutConfirmationDialog } from './components/CheckoutConfirmationDialog'; import { RaceCreationSuccessScreen } from './components/RaceCreationSuccessScreen'; import type { HostedSessionConfig } from '../../../packages/automation/domain/types/HostedSessionConfig'; +import type { AuthenticationState } from '../../../packages/automation/domain/value-objects/AuthenticationState'; +import type { StartAutomationResponse } from '../main/preload'; interface SessionProgress { sessionId: string; @@ -16,7 +18,7 @@ interface SessionProgress { errorMessage: string | null; } -type AuthState = 'UNKNOWN' | 'AUTHENTICATED' | 'EXPIRED' | 'LOGGED_OUT' | 'CHECKING'; +type AuthState = AuthenticationState | 'CHECKING'; type LoginStatus = 'idle' | 'waiting' | 'success' | 'error'; export function App() { @@ -84,7 +86,7 @@ export function App() { try { const result = await window.electronAPI.checkAuth(); if (result.success && result.state) { - setAuthState(result.state as AuthState); + setAuthState(result.state); } else { setAuthError(result.error); setAuthState('UNKNOWN'); @@ -103,7 +105,7 @@ export function App() { try { const result = await window.electronAPI.checkAuth(); if (result.success && result.state) { - setAuthState(result.state as AuthState); + setAuthState(result.state); } else { setAuthError(result.error || 'Failed to check authentication'); setAuthState('UNKNOWN'); @@ -138,13 +140,7 @@ export function App() { const handleStartAutomation = async (config: HostedSessionConfig) => { setIsRunning(true); - const result = await window.electronAPI.startAutomation(config) as { - success: boolean; - sessionId?: string; - error?: string; - authRequired?: boolean; - authState?: AuthState; - }; + const result: StartAutomationResponse = await window.electronAPI.startAutomation(config); if (result.success && result.sessionId) { setSessionId(result.sessionId); @@ -153,8 +149,8 @@ export function App() { setIsRunning(false); - if ('authRequired' in result && result.authRequired) { - const nextAuthState = result.authState as AuthState | undefined; + if (result.authRequired) { + const nextAuthState = result.authState; setAuthState(nextAuthState ?? 'EXPIRED'); setAuthError(result.error ?? 'Authentication required before starting automation.'); return; diff --git a/apps/website/app/api/sponsors/dashboard/route.ts b/apps/website/app/api/sponsors/dashboard/route.ts index 544716e66..e13180d53 100644 --- a/apps/website/app/api/sponsors/dashboard/route.ts +++ b/apps/website/app/api/sponsors/dashboard/route.ts @@ -19,7 +19,7 @@ export async function GET(request: NextRequest) { const presenter = new SponsorDashboardPresenter(); const useCase = getGetSponsorDashboardUseCase(); - await useCase.execute({ sponsorId }); + await useCase.execute({ sponsorId }, presenter); const dashboard = presenter.getData(); if (!dashboard) { diff --git a/apps/website/app/api/sponsors/sponsorships/route.ts b/apps/website/app/api/sponsors/sponsorships/route.ts index cf8e4a2fd..e5093f0bd 100644 --- a/apps/website/app/api/sponsors/sponsorships/route.ts +++ b/apps/website/app/api/sponsors/sponsorships/route.ts @@ -19,7 +19,7 @@ export async function GET(request: NextRequest) { const presenter = new SponsorSponsorshipsPresenter(); const useCase = getGetSponsorSponsorshipsUseCase(); - await useCase.execute({ sponsorId }); + await useCase.execute({ sponsorId }, presenter); const sponsorships = presenter.getData(); if (!sponsorships) { diff --git a/apps/website/app/auth/login/page.tsx b/apps/website/app/auth/login/page.tsx index b9ff210a4..cf94ebb02 100644 --- a/apps/website/app/auth/login/page.tsx +++ b/apps/website/app/auth/login/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, FormEvent } from 'react'; +import { useState, FormEvent, type ChangeEvent } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { @@ -145,7 +145,7 @@ export default function LoginPage() { id="email" type="email" value={formData.email} - onChange={(e) => setFormData({ ...formData, email: e.target.value })} + onChange={(e: ChangeEvent) => setFormData({ ...formData, email: e.target.value })} error={!!errors.email} errorMessage={errors.email} placeholder="you@example.com" @@ -172,7 +172,7 @@ export default function LoginPage() { id="password" type={showPassword ? 'text' : 'password'} value={formData.password} - onChange={(e) => setFormData({ ...formData, password: e.target.value })} + onChange={(e: ChangeEvent) => setFormData({ ...formData, password: e.target.value })} error={!!errors.password} errorMessage={errors.password} placeholder="••••••••" diff --git a/apps/website/app/auth/signup/page.tsx b/apps/website/app/auth/signup/page.tsx index f8291646c..ebccef008 100644 --- a/apps/website/app/auth/signup/page.tsx +++ b/apps/website/app/auth/signup/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, FormEvent } from 'react'; +import { useState, useEffect, FormEvent, type ChangeEvent } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { @@ -226,7 +226,7 @@ export default function SignupPage() { id="displayName" type="text" value={formData.displayName} - onChange={(e) => setFormData({ ...formData, displayName: e.target.value })} + onChange={(e: ChangeEvent) => setFormData({ ...formData, displayName: e.target.value })} error={!!errors.displayName} errorMessage={errors.displayName} placeholder="SpeedyRacer42" @@ -249,7 +249,7 @@ export default function SignupPage() { id="email" type="email" value={formData.email} - onChange={(e) => setFormData({ ...formData, email: e.target.value })} + onChange={(e: ChangeEvent) => setFormData({ ...formData, email: e.target.value })} error={!!errors.email} errorMessage={errors.email} placeholder="you@example.com" @@ -271,7 +271,7 @@ export default function SignupPage() { id="password" type={showPassword ? 'text' : 'password'} value={formData.password} - onChange={(e) => setFormData({ ...formData, password: e.target.value })} + onChange={(e: ChangeEvent) => setFormData({ ...formData, password: e.target.value })} error={!!errors.password} errorMessage={errors.password} placeholder="••••••••" @@ -336,7 +336,7 @@ export default function SignupPage() { id="confirmPassword" type={showConfirmPassword ? 'text' : 'password'} value={formData.confirmPassword} - onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })} + onChange={(e: ChangeEvent) => setFormData({ ...formData, confirmPassword: e.target.value })} error={!!errors.confirmPassword} errorMessage={errors.confirmPassword} placeholder="••••••••" diff --git a/apps/website/app/dashboard/page.tsx b/apps/website/app/dashboard/page.tsx index 0a20eb1fe..1c516a556 100644 --- a/apps/website/app/dashboard/page.tsx +++ b/apps/website/app/dashboard/page.tsx @@ -27,6 +27,7 @@ import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import { getAuthService } from '@/lib/auth'; import { getGetDashboardOverviewUseCase } from '@/lib/di-container'; +import { DashboardOverviewPresenter } from '@/lib/presenters/DashboardOverviewPresenter'; import type { DashboardOverviewViewModel, DashboardFeedItemSummaryViewModel, @@ -94,8 +95,9 @@ export default async function DashboardPage() { const currentDriverId = session.user.primaryDriverId ?? ''; const useCase = getGetDashboardOverviewUseCase(); - await useCase.execute({ driverId: currentDriverId }); - const viewModel = useCase.presenter.getViewModel() as DashboardOverviewViewModel | null; + const presenter = new DashboardOverviewPresenter(); + await useCase.execute({ driverId: currentDriverId }, presenter); + const viewModel = presenter.getViewModel(); if (!viewModel) { return null; diff --git a/apps/website/app/drivers/page.tsx b/apps/website/app/drivers/page.tsx index 9c6bfed74..b2f5dfd54 100644 --- a/apps/website/app/drivers/page.tsx +++ b/apps/website/app/drivers/page.tsx @@ -28,6 +28,7 @@ import Input from '@/components/ui/Input'; import Card from '@/components/ui/Card'; import Heading from '@/components/ui/Heading'; import { getGetDriversLeaderboardUseCase } from '@/lib/di-container'; +import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; import Image from 'next/image'; @@ -387,13 +388,16 @@ export default function DriversPage() { useEffect(() => { const load = async () => { const useCase = getGetDriversLeaderboardUseCase(); - await useCase.execute(); - const viewModel = useCase.presenter.getViewModel(); + const presenter = new DriversLeaderboardPresenter(); + await useCase.execute(undefined as void, presenter); + const viewModel = presenter.getViewModel(); - setDrivers(viewModel.drivers); - setTotalRaces(viewModel.totalRaces); - setTotalWins(viewModel.totalWins); - setActiveCount(viewModel.activeCount); + if (viewModel) { + setDrivers(viewModel.drivers); + setTotalRaces(viewModel.totalRaces); + setTotalWins(viewModel.totalWins); + setActiveCount(viewModel.activeCount); + } setLoading(false); }; diff --git a/apps/website/app/leaderboards/drivers/page.tsx b/apps/website/app/leaderboards/drivers/page.tsx index dea9e37db..153d79fc3 100644 --- a/apps/website/app/leaderboards/drivers/page.tsx +++ b/apps/website/app/leaderboards/drivers/page.tsx @@ -20,6 +20,7 @@ import Button from '@/components/ui/Button'; import Input from '@/components/ui/Input'; import Heading from '@/components/ui/Heading'; import { getGetDriversLeaderboardUseCase } from '@/lib/di-container'; +import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; import Image from 'next/image'; @@ -181,9 +182,12 @@ export default function DriverLeaderboardPage() { useEffect(() => { const load = async () => { const useCase = getGetDriversLeaderboardUseCase(); - await useCase.execute(); - const viewModel = useCase.presenter.getViewModel(); - setDrivers(viewModel.drivers); + const presenter = new DriversLeaderboardPresenter(); + await useCase.execute(undefined as void, presenter); + const viewModel = presenter.getViewModel(); + if (viewModel) { + setDrivers(viewModel.drivers); + } setLoading(false); }; diff --git a/apps/website/app/leaderboards/page.tsx b/apps/website/app/leaderboards/page.tsx index dfbf9f274..1c8a252b9 100644 --- a/apps/website/app/leaderboards/page.tsx +++ b/apps/website/app/leaderboards/page.tsx @@ -21,6 +21,7 @@ import { import Button from '@/components/ui/Button'; import Heading from '@/components/ui/Heading'; import { getGetDriversLeaderboardUseCase, getGetTeamsLeaderboardUseCase } from '@/lib/di-container'; +import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter'; import { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; import type { TeamLeaderboardItemViewModel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter'; @@ -287,15 +288,16 @@ export default function LeaderboardsPage() { try { const driversUseCase = getGetDriversLeaderboardUseCase(); const teamsUseCase = getGetTeamsLeaderboardUseCase(); + const driversPresenter = new DriversLeaderboardPresenter(); const teamsPresenter = new TeamsLeaderboardPresenter(); - await driversUseCase.execute(); + await driversUseCase.execute(undefined as void, driversPresenter); await teamsUseCase.execute(undefined as void, teamsPresenter); - const driversViewModel = driversUseCase.presenter.getViewModel(); + const driversViewModel = driversPresenter.getViewModel(); const teamsViewModel = teamsPresenter.getViewModel(); - setDrivers(driversViewModel.drivers); + setDrivers(driversViewModel?.drivers ?? []); setTeams(teamsViewModel ? teamsViewModel.teams : []); } catch (error) { console.error('Failed to load leaderboard data:', error); diff --git a/apps/website/app/leagues/[id]/page.tsx b/apps/website/app/leagues/[id]/page.tsx index 963e4f4ce..3334cdaf4 100644 --- a/apps/website/app/leagues/[id]/page.tsx +++ b/apps/website/app/leagues/[id]/page.tsx @@ -33,6 +33,7 @@ import { getSponsorRepository, getSeasonSponsorshipRepository, } from '@/lib/di-container'; +import { LeagueScoringConfigPresenter } from '@/lib/presenters/LeagueScoringConfigPresenter'; import { Trophy, Star, ExternalLink } from 'lucide-react'; import { getMembership, getLeagueMembers } from '@/lib/leagueMembership'; import { useEffectiveDriverId } from '@/lib/currentDriver'; @@ -125,8 +126,9 @@ export default function LeagueDetailPage() { // Load scoring configuration for the active season const getLeagueScoringConfigUseCase = getGetLeagueScoringConfigUseCase(); - await getLeagueScoringConfigUseCase.execute({ leagueId }); - const scoringViewModel = getLeagueScoringConfigUseCase.presenter.getViewModel(); + const scoringPresenter = new LeagueScoringConfigPresenter(); + await getLeagueScoringConfigUseCase.execute({ leagueId }, scoringPresenter); + const scoringViewModel = scoringPresenter.getViewModel(); setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO); // Load all drivers for standings and map to DTOs for UI components diff --git a/apps/website/app/leagues/[id]/rulebook/page.tsx b/apps/website/app/leagues/[id]/rulebook/page.tsx index 4d7b1b99e..04ec4eaea 100644 --- a/apps/website/app/leagues/[id]/rulebook/page.tsx +++ b/apps/website/app/leagues/[id]/rulebook/page.tsx @@ -7,6 +7,7 @@ import { getLeagueRepository, getGetLeagueScoringConfigUseCase } from '@/lib/di-container'; +import { LeagueScoringConfigPresenter } from '@/lib/presenters/LeagueScoringConfigPresenter'; import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO'; import type { League } from '@gridpilot/racing/domain/entities/League'; @@ -35,8 +36,9 @@ export default function LeagueRulebookPage() { setLeague(leagueData); - await scoringUseCase.execute({ leagueId }); - const scoringViewModel = scoringUseCase.presenter.getViewModel(); + const scoringPresenter = new LeagueScoringConfigPresenter(); + await scoringUseCase.execute({ leagueId }, scoringPresenter); + const scoringViewModel = scoringPresenter.getViewModel(); setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO); } catch (err) { console.error('Failed to load scoring config:', err); diff --git a/apps/website/app/leagues/[id]/standings/page.tsx b/apps/website/app/leagues/[id]/standings/page.tsx index c92caad87..a4fab960a 100644 --- a/apps/website/app/leagues/[id]/standings/page.tsx +++ b/apps/website/app/leagues/[id]/standings/page.tsx @@ -14,6 +14,7 @@ import { getDriverRepository, getLeagueMembershipRepository } from '@/lib/di-container'; +import { LeagueDriverSeasonStatsPresenter } from '@/lib/presenters/LeagueDriverSeasonStatsPresenter'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import type { MembershipRole, LeagueMembership } from '@/lib/leagueMembership'; @@ -36,15 +37,9 @@ export default function LeagueStandingsPage() { const driverRepo = getDriverRepository(); const membershipRepo = getLeagueMembershipRepository(); - await getLeagueDriverSeasonStatsUseCase.execute({ leagueId }); - type GetLeagueDriverSeasonStatsUseCaseType = { - presenter: { - getViewModel(): { stats: LeagueDriverSeasonStatsDTO[] }; - }; - }; - const typedUseCase = - getLeagueDriverSeasonStatsUseCase as GetLeagueDriverSeasonStatsUseCaseType; - const standingsViewModel = typedUseCase.presenter.getViewModel(); + const presenter = new LeagueDriverSeasonStatsPresenter(); + await getLeagueDriverSeasonStatsUseCase.execute({ leagueId }, presenter); + const standingsViewModel = presenter.getViewModel(); setStandings(standingsViewModel.stats); const allDrivers = await driverRepo.findAll(); diff --git a/apps/website/app/leagues/page.tsx b/apps/website/app/leagues/page.tsx index b19b6aa5b..74c89edda 100644 --- a/apps/website/app/leagues/page.tsx +++ b/apps/website/app/leagues/page.tsx @@ -30,7 +30,8 @@ import Button from '@/components/ui/Button'; import Card from '@/components/ui/Card'; import Input from '@/components/ui/Input'; import Heading from '@/components/ui/Heading'; -import type { LeagueSummaryDTO } from '@gridpilot/racing/application/dto/LeagueSummaryDTO'; +import type { LeagueSummaryViewModel } from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter'; +import { AllLeaguesWithCapacityAndScoringPresenter } from '@/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter'; import { getGetAllLeaguesWithCapacityAndScoringUseCase } from '@/lib/di-container'; // ============================================================================ @@ -57,7 +58,7 @@ interface Category { label: string; icon: React.ElementType; description: string; - filter: (league: LeagueSummaryDTO) => boolean; + filter: (league: LeagueSummaryViewModel) => boolean; color?: string; } @@ -175,7 +176,7 @@ interface LeagueSliderProps { title: string; icon: React.ElementType; description: string; - leagues: LeagueSummaryDTO[]; + leagues: LeagueSummaryViewModel[]; onLeagueClick: (id: string) => void; autoScroll?: boolean; iconColor?: string; @@ -377,25 +378,23 @@ function LeagueSlider({ export default function LeaguesPage() { const router = useRouter(); - const [realLeagues, setRealLeagues] = useState([]); + const [realLeagues, setRealLeagues] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [activeCategory, setActiveCategory] = useState('all'); const [showFilters, setShowFilters] = useState(false); useEffect(() => { - loadLeagues(); + void loadLeagues(); }, []); const loadLeagues = async () => { try { const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase(); - await useCase.execute(); - const presenter = useCase.presenter as unknown as { - getViewModel(): { leagues: LeagueSummaryDTO[] }; - }; + const presenter = new AllLeaguesWithCapacityAndScoringPresenter(); + await useCase.execute(undefined as void, presenter); const viewModel = presenter.getViewModel(); - setRealLeagues(viewModel.leagues); + setRealLeagues(viewModel?.leagues ?? []); } catch (error) { console.error('Failed to load leagues:', error); } finally { @@ -434,7 +433,7 @@ export default function LeaguesPage() { acc[category.id] = searchFilteredLeagues.filter(category.filter); return acc; }, - {} as Record, + {} as Record, ); // Featured categories to show as sliders with different scroll speeds and alternating directions diff --git a/apps/website/app/races/[id]/page.tsx b/apps/website/app/races/[id]/page.tsx index 3a6b49d93..0b110272a 100644 --- a/apps/website/app/races/[id]/page.tsx +++ b/apps/website/app/races/[id]/page.tsx @@ -34,6 +34,7 @@ import { ArrowLeft, Scale, } from 'lucide-react'; +import { RaceDetailPresenter } from '@/lib/presenters/RaceDetailPresenter'; export default function RaceDetailPage() { const router = useRouter(); @@ -57,8 +58,9 @@ export default function RaceDetailPage() { setError(null); try { const useCase = getGetRaceDetailUseCase(); - await useCase.execute({ raceId, driverId: currentDriverId }); - const vm = useCase.presenter.getViewModel(); + const presenter = new RaceDetailPresenter(); + await useCase.execute({ raceId, driverId: currentDriverId }, presenter); + const vm = presenter.getViewModel(); if (!vm) { throw new Error('Race detail not available'); } diff --git a/apps/website/app/races/[id]/results/page.tsx b/apps/website/app/races/[id]/results/page.tsx index 04a04f371..ebb2d83b8 100644 --- a/apps/website/app/races/[id]/results/page.tsx +++ b/apps/website/app/races/[id]/results/page.tsx @@ -13,6 +13,8 @@ import { getGetRaceResultsDetailUseCase, getImportRaceResultsUseCase, } from '@/lib/di-container'; +import { RaceWithSOFPresenter } from '@/lib/presenters/RaceWithSOFPresenter'; +import { RaceResultsDetailPresenter } from '@/lib/presenters/RaceResultsDetailPresenter'; import type { RaceResultsHeaderViewModel, RaceResultsLeagueViewModel, @@ -71,7 +73,7 @@ export default function RaceResultsPage() { const [currentDriverId, setCurrentDriverId] = useState(undefined); const [raceSOF, setRaceSOF] = useState(null); const [penalties, setPenalties] = useState([]); - const [pointsSystem, setPointsSystem] = useState>({}); + const [pointsSystem, setPointsSystem] = useState | undefined>(undefined); const [fastestLapTime, setFastestLapTime] = useState(undefined); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -81,9 +83,10 @@ export default function RaceResultsPage() { const loadData = async () => { try { const raceResultsUseCase = getGetRaceResultsDetailUseCase(); - await raceResultsUseCase.execute({ raceId }); + const raceResultsPresenter = new RaceResultsDetailPresenter(); + await raceResultsUseCase.execute({ raceId }, raceResultsPresenter); - const viewModel = raceResultsUseCase.presenter.getViewModel(); + const viewModel = raceResultsPresenter.getViewModel(); if (!viewModel) { setError('Failed to load race data'); @@ -130,8 +133,9 @@ export default function RaceResultsPage() { try { const raceWithSOFUseCase = getGetRaceWithSOFUseCase(); - await raceWithSOFUseCase.execute({ raceId }); - const raceViewModel = raceWithSOFUseCase.presenter.getViewModel(); + const sofPresenter = new RaceWithSOFPresenter(); + await raceWithSOFUseCase.execute({ raceId }, sofPresenter); + const raceViewModel = sofPresenter.getViewModel(); if (raceViewModel) { setRaceSOF(raceViewModel.strengthOfField); } @@ -290,7 +294,7 @@ export default function RaceResultsPage() { { try { const useCase = getGetAllRacesPageDataUseCase(); - await useCase.execute(); - const viewModel = useCase.presenter.getViewModel(); + const presenter = new AllRacesPagePresenter(); + await useCase.execute(undefined, presenter); + const viewModel = presenter.getViewModel(); setPageData(viewModel); } catch (err) { console.error('Failed to load races:', err); diff --git a/apps/website/app/races/page.tsx b/apps/website/app/races/page.tsx index 3507cbc21..c5e479933 100644 --- a/apps/website/app/races/page.tsx +++ b/apps/website/app/races/page.tsx @@ -7,6 +7,7 @@ import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Heading from '@/components/ui/Heading'; import { getGetRacesPageDataUseCase } from '@/lib/di-container'; +import { RacesPagePresenter } from '@/lib/presenters/RacesPagePresenter'; import type { RacesPageViewModel, RaceListItemViewModel, @@ -46,8 +47,9 @@ export default function RacesPage() { const loadRaces = async () => { try { const useCase = getGetRacesPageDataUseCase(); - await useCase.execute(); - const data = useCase.presenter.getViewModel(); + const presenter = new RacesPagePresenter(); + await useCase.execute(undefined, presenter); + const data = presenter.getViewModel(); setPageData(data); } catch (err) { console.error('Failed to load races:', err); diff --git a/apps/website/app/teams/[id]/page.tsx b/apps/website/app/teams/[id]/page.tsx index cfd5cde7f..587fdd0e3 100644 --- a/apps/website/app/teams/[id]/page.tsx +++ b/apps/website/app/teams/[id]/page.tsx @@ -10,6 +10,8 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs'; import SponsorInsightsCard, { useSponsorMode, MetricBuilders, SlotTemplates } from '@/components/sponsors/SponsorInsightsCard'; import { getImageService } from '@/lib/di-container'; import { TeamMembersPresenter } from '@/lib/presenters/TeamMembersPresenter'; +import { TeamDetailsPresenter } from '@/lib/presenters/TeamDetailsPresenter'; +import type { TeamDetailsViewModel } from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter'; import TeamRoster from '@/components/teams/TeamRoster'; import TeamStandings from '@/components/teams/TeamStandings'; import TeamAdmin from '@/components/teams/TeamAdmin'; @@ -20,7 +22,6 @@ import { getTeamMembershipRepository, } from '@/lib/di-container'; import { useEffectiveDriverId } from '@/lib/currentDriver'; -import type { Team } from '@gridpilot/racing'; import { Users, Trophy, TrendingUp, Star, Zap } from 'lucide-react'; type TeamRole = 'owner' | 'manager' | 'driver'; @@ -36,8 +37,10 @@ type Tab = 'overview' | 'roster' | 'standings' | 'admin'; export default function TeamDetailPage() { const params = useParams(); const teamId = params.id as string; - - const [team, setTeam] = useState(null); + + type TeamViewModel = TeamDetailsViewModel['team']; + + const [team, setTeam] = useState(null); const [memberships, setMemberships] = useState([]); const [activeTab, setActiveTab] = useState('overview'); const [loading, setLoading] = useState(true); @@ -51,11 +54,9 @@ export default function TeamDetailPage() { const detailsUseCase = getGetTeamDetailsUseCase(); const membersUseCase = getGetTeamMembersUseCase(); - await detailsUseCase.execute(teamId, currentDriverId); - const detailsPresenter = detailsUseCase.presenter; - const detailsViewModel = detailsPresenter - ? (detailsPresenter as any).getViewModel?.() as { team: Team } | null - : null; + const detailsPresenter = new TeamDetailsPresenter(); + await detailsUseCase.execute({ teamId, driverId: currentDriverId }, detailsPresenter); + const detailsViewModel = detailsPresenter.getViewModel(); if (!detailsViewModel) { setTeam(null); diff --git a/apps/website/components/leagues/LeagueCard.tsx b/apps/website/components/leagues/LeagueCard.tsx index df4cbdf71..fc4c6ed03 100644 --- a/apps/website/components/leagues/LeagueCard.tsx +++ b/apps/website/components/leagues/LeagueCard.tsx @@ -12,12 +12,12 @@ import { ChevronRight, Sparkles, } from 'lucide-react'; -import type { LeagueSummaryDTO } from '@gridpilot/racing/application/dto/LeagueSummaryDTO'; +import type { LeagueSummaryViewModel } from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter'; import { getLeagueCoverClasses } from '@/lib/leagueCovers'; import { getImageService } from '@/lib/di-container'; interface LeagueCardProps { - league: LeagueSummaryDTO; + league: LeagueSummaryViewModel; onClick?: () => void; } @@ -65,7 +65,7 @@ function getGameColor(gameId?: string): string { } } -function isNewLeague(createdAt: Date): boolean { +function isNewLeague(createdAt: string | Date): boolean { const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); return new Date(createdAt) > oneWeekAgo; diff --git a/apps/website/components/mockups/DriverProfileMockup.tsx b/apps/website/components/mockups/DriverProfileMockup.tsx index da67aed0c..598ee73e8 100644 --- a/apps/website/components/mockups/DriverProfileMockup.tsx +++ b/apps/website/components/mockups/DriverProfileMockup.tsx @@ -251,7 +251,7 @@ export default function DriverProfileMockup() { function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boolean; value: number }) { const count = useMotionValue(0); - const rounded = useTransform(count, (v) => Math.round(v)); + const rounded = useTransform(count, (v: number) => Math.round(v)); const spring = useSpring(count, { stiffness: 50, damping: 25 }); useEffect(() => { @@ -282,7 +282,7 @@ function AnimatedCounter({ suffix?: string; }) { const count = useMotionValue(0); - const rounded = useTransform(count, (v) => Math.round(v)); + const rounded = useTransform(count, (v: number) => Math.round(v)); useEffect(() => { if (shouldReduceMotion) { diff --git a/apps/website/components/mockups/RatingFactorsMockup.tsx b/apps/website/components/mockups/RatingFactorsMockup.tsx index e7d452518..4f1bbc43f 100644 --- a/apps/website/components/mockups/RatingFactorsMockup.tsx +++ b/apps/website/components/mockups/RatingFactorsMockup.tsx @@ -146,7 +146,7 @@ function RatingFactor({ }) { const progress = useMotionValue(0); const smoothProgress = useSpring(progress, { stiffness: 60, damping: 25 }); - const width = useTransform(smoothProgress, (v) => `${v}%`); + const width = useTransform(smoothProgress, (v: number) => `${v}%`); useEffect(() => { if (shouldReduceMotion) { @@ -187,7 +187,7 @@ function RatingFactor({ function AnimatedRating({ shouldReduceMotion }: { shouldReduceMotion: boolean }) { const count = useMotionValue(0); - const rounded = useTransform(count, (v) => Math.round(v)); + const rounded = useTransform(count, (v: number) => Math.round(v)); const spring = useSpring(count, { stiffness: 50, damping: 25 }); useEffect(() => { diff --git a/apps/website/env.d.ts b/apps/website/env.d.ts index 23da69103..452950c37 100644 --- a/apps/website/env.d.ts +++ b/apps/website/env.d.ts @@ -1,6 +1,55 @@ /// /// +declare module 'framer-motion' { + import type { ComponentType } from 'react'; + + // Minimal shim to satisfy strict typing for usage in JSX + export type MotionComponent = ComponentType>; + + export const motion: { + div: MotionComponent; + span: MotionComponent; + button: MotionComponent; + svg: MotionComponent; + p: MotionComponent; + [element: string]: MotionComponent; + }; + + export const AnimatePresence: ComponentType>; + + export function useReducedMotion(): boolean; + + // Shim motion values with a minimal interface exposing .set() + export interface MotionValue { + get(): T; + set(v: T): void; + } + + export function useMotionValue(initial: number): MotionValue; + export function useSpring(value: MotionValue | number, config?: Record): MotionValue; + export function useTransform( + value: MotionValue, + transformer: (input: TInput) => TOutput, + ): MotionValue; +} + +declare module '@next/third-party-devtools' { + import type { ComponentType } from 'react'; + const Devtools: ComponentType>; + export default Devtools; +} + +declare module 'react/compiler-runtime' { + export {}; +} + +// Shim missing React namespace member used by Next devtools types +declare namespace React { + // Minimal placeholder type; generic to match Next's usage + type ActionDispatch = (action: T) => void; +} + declare namespace NodeJS { interface ProcessEnv { NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha'; diff --git a/apps/website/lib/di-config.ts b/apps/website/lib/di-config.ts index 58283e155..ac7b49f47 100644 --- a/apps/website/lib/di-config.ts +++ b/apps/website/lib/di-config.ts @@ -195,15 +195,15 @@ export function configureDIContainer(): void { // Create driver statistics from seed data type DemoDriverStatsEntry = { - rating?: number; - wins?: number; - podiums?: number; + rating: number; + wins: number; + podiums: number; + totalRaces: number; + overallRank: number | null; dnfs?: number; - totalRaces?: number; avgFinish?: number; bestFinish?: number; worstFinish?: number; - overallRank?: number; consistency?: number; percentile?: number; driverId?: string; @@ -952,10 +952,9 @@ export function configureDIContainer(): void { new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter) ); - const raceRegistrationsPresenter = new RaceRegistrationsPresenter(); container.registerInstance( DI_TOKENS.GetRaceRegistrationsUseCase, - new GetRaceRegistrationsUseCase(raceRegistrationRepository, raceRegistrationsPresenter) + new GetRaceRegistrationsUseCase(raceRegistrationRepository) ); const leagueStandingsPresenter = new LeagueStandingsPresenter(); @@ -964,7 +963,6 @@ export function configureDIContainer(): void { new GetLeagueStandingsUseCase(standingRepository), ); - const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter(); container.registerInstance( DI_TOKENS.GetLeagueDriverSeasonStatsUseCase, new GetLeagueDriverSeasonStatsUseCase( @@ -986,21 +984,17 @@ export function configureDIContainer(): void { }; }, }, - leagueDriverSeasonStatsPresenter, ), ); - const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter(); container.registerInstance( DI_TOKENS.GetAllLeaguesWithCapacityUseCase, new GetAllLeaguesWithCapacityUseCase( leagueRepository, leagueMembershipRepository, - allLeaguesWithCapacityPresenter ) ); - const allLeaguesWithCapacityAndScoringPresenter = new AllLeaguesWithCapacityAndScoringPresenter(); container.registerInstance( DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase, new GetAllLeaguesWithCapacityAndScoringUseCase( @@ -1010,7 +1004,6 @@ export function configureDIContainer(): void { leagueScoringConfigRepository, gameRepository, leagueScoringPresetProvider, - allLeaguesWithCapacityAndScoringPresenter ) ); @@ -1020,7 +1013,6 @@ export function configureDIContainer(): void { new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider) ); - const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter(); container.registerInstance( DI_TOKENS.GetLeagueScoringConfigUseCase, new GetLeagueScoringConfigUseCase( @@ -1029,7 +1021,6 @@ export function configureDIContainer(): void { leagueScoringConfigRepository, gameRepository, leagueScoringPresetProvider, - leagueScoringConfigPresenter ) ); @@ -1049,7 +1040,6 @@ export function configureDIContainer(): void { new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter), ); - const raceWithSOFPresenter = new RaceWithSOFPresenter(); container.registerInstance( DI_TOKENS.GetRaceWithSOFUseCase, new GetRaceWithSOFUseCase( @@ -1057,7 +1047,6 @@ export function configureDIContainer(): void { raceRegistrationRepository, resultRepository, driverRatingProvider, - raceWithSOFPresenter ) ); @@ -1073,21 +1062,18 @@ export function configureDIContainer(): void { ) ); - const racesPresenter = new RacesPagePresenter(); container.registerInstance( DI_TOKENS.GetRacesPageDataUseCase, - new GetRacesPageDataUseCase(raceRepository, leagueRepository, racesPresenter) + new GetRacesPageDataUseCase(raceRepository, leagueRepository) ); - const allRacesPagePresenter = new AllRacesPagePresenter(); container.registerInstance( DI_TOKENS.GetAllRacesPageDataUseCase, - new GetAllRacesPageDataUseCase(raceRepository, leagueRepository, allRacesPagePresenter) + new GetAllRacesPageDataUseCase(raceRepository, leagueRepository) ); const imageService = container.resolve(DI_TOKENS.ImageService); - const raceDetailPresenter = new RaceDetailPresenter(); container.registerInstance( DI_TOKENS.GetRaceDetailUseCase, new GetRaceDetailUseCase( @@ -1099,11 +1085,9 @@ export function configureDIContainer(): void { leagueMembershipRepository, driverRatingProvider, imageService, - raceDetailPresenter ) ); - const raceResultsDetailPresenter = new RaceResultsDetailPresenter(); container.registerInstance( DI_TOKENS.GetRaceResultsDetailUseCase, new GetRaceResultsDetailUseCase( @@ -1112,7 +1096,6 @@ export function configureDIContainer(): void { resultRepository, driverRepository, penaltyRepository, - raceResultsDetailPresenter ) ); @@ -1149,7 +1132,6 @@ export function configureDIContainer(): void { }, }; - const driversPresenter = new DriversLeaderboardPresenter(); container.registerInstance( DI_TOKENS.GetDriversLeaderboardUseCase, new GetDriversLeaderboardUseCase( @@ -1157,7 +1139,6 @@ export function configureDIContainer(): void { rankingService, driverStatsService, imageService, - driversPresenter ) ); @@ -1215,7 +1196,6 @@ export function configureDIContainer(): void { }; }; - const dashboardOverviewPresenter = new DashboardOverviewPresenter(); container.registerInstance( DI_TOKENS.GetDashboardOverviewUseCase, new GetDashboardOverviewUseCase( @@ -1230,7 +1210,6 @@ export function configureDIContainer(): void { socialRepository, imageService, getDriverStatsForDashboard, - dashboardOverviewPresenter ) ); @@ -1260,11 +1239,10 @@ export function configureDIContainer(): void { DI_TOKENS.GetAllTeamsUseCase, new GetAllTeamsUseCase(teamRepository, teamMembershipRepository), ); - - const teamDetailsPresenter = new TeamDetailsPresenter(); + container.registerInstance( DI_TOKENS.GetTeamDetailsUseCase, - new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository, teamDetailsPresenter) + new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository) ); const teamMembersPresenter = new TeamMembersPresenter(); @@ -1313,7 +1291,6 @@ export function configureDIContainer(): void { const sponsorRepository = container.resolve(DI_TOKENS.SponsorRepository); const seasonSponsorshipRepository = container.resolve(DI_TOKENS.SeasonSponsorshipRepository); - const sponsorDashboardPresenter = new SponsorDashboardPresenter(); container.registerInstance( DI_TOKENS.GetSponsorDashboardUseCase, new GetSponsorDashboardUseCase( @@ -1323,11 +1300,9 @@ export function configureDIContainer(): void { leagueRepository, leagueMembershipRepository, raceRepository, - sponsorDashboardPresenter ) ); - const sponsorSponsorshipsPresenter = new SponsorSponsorshipsPresenter(); container.registerInstance( DI_TOKENS.GetSponsorSponsorshipsUseCase, new GetSponsorSponsorshipsUseCase( @@ -1337,7 +1312,6 @@ export function configureDIContainer(): void { leagueRepository, leagueMembershipRepository, raceRepository, - sponsorSponsorshipsPresenter ) ); diff --git a/apps/website/lib/di-container.ts b/apps/website/lib/di-container.ts index d3cade6fd..e4d425e89 100644 --- a/apps/website/lib/di-container.ts +++ b/apps/website/lib/di-container.ts @@ -625,7 +625,7 @@ export function getIsDriverRegisteredForRaceQuery(): { return { async execute(input: { raceId: string; driverId: string }): Promise { const result = await useCase.execute(input); - return result as unknown as boolean; + return Boolean(result); }, }; } diff --git a/apps/website/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter.ts b/apps/website/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter.ts index 885e16e8c..8e53c7e3d 100644 --- a/apps/website/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter.ts +++ b/apps/website/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter.ts @@ -1,4 +1,3 @@ -import type { League } from '@gridpilot/racing/domain/entities/League'; import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData, @@ -9,7 +8,11 @@ import type { export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter { private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null; - present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel { + reset(): void { + this.viewModel = null; + } + + present(enrichedLeagues: LeagueEnrichedData[]): void { const leagueItems: LeagueSummaryViewModel[] = enrichedLeagues.map((data) => { const { league, usedDriverSlots, season, scoringConfig, game, preset } = data; @@ -68,7 +71,7 @@ export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWit name: league.name, description: league.description, ownerId: league.ownerId, - createdAt: league.createdAt, + createdAt: league.createdAt.toISOString(), maxDrivers: safeMaxDrivers, usedDriverSlots, // Team capacity is not yet modeled here; use zero for now to satisfy strict typing. @@ -87,14 +90,9 @@ export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWit leagues: leagueItems, totalCount: leagueItems.length, }; - - return this.viewModel; } - getViewModel(): AllLeaguesWithCapacityAndScoringViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } + getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null { return this.viewModel; } diff --git a/apps/website/lib/presenters/AllLeaguesWithCapacityPresenter.ts b/apps/website/lib/presenters/AllLeaguesWithCapacityPresenter.ts index 6eb1db65d..82f65eaf0 100644 --- a/apps/website/lib/presenters/AllLeaguesWithCapacityPresenter.ts +++ b/apps/website/lib/presenters/AllLeaguesWithCapacityPresenter.ts @@ -1,17 +1,19 @@ -import type { League } from '@gridpilot/racing/domain/entities/League'; import type { IAllLeaguesWithCapacityPresenter, LeagueWithCapacityViewModel, AllLeaguesWithCapacityViewModel, + AllLeaguesWithCapacityResultDTO, } from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter'; export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter { private viewModel: AllLeaguesWithCapacityViewModel | null = null; - present( - leagues: League[], - memberCounts: Map - ): AllLeaguesWithCapacityViewModel { + reset(): void { + this.viewModel = null; + } + + present(input: AllLeaguesWithCapacityResultDTO): void { + const { leagues, memberCounts } = input; const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => { const usedSlots = memberCounts.get(league.id) ?? 0; @@ -63,14 +65,9 @@ export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityP leagues: leagueItems, totalCount: leagueItems.length, }; - - return this.viewModel; } - getViewModel(): AllLeaguesWithCapacityViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } + getViewModel(): AllLeaguesWithCapacityViewModel | null { return this.viewModel; } } \ No newline at end of file diff --git a/apps/website/lib/presenters/AllRacesPagePresenter.ts b/apps/website/lib/presenters/AllRacesPagePresenter.ts index 418a8aab5..ec6fe3e8f 100644 --- a/apps/website/lib/presenters/AllRacesPagePresenter.ts +++ b/apps/website/lib/presenters/AllRacesPagePresenter.ts @@ -1,13 +1,18 @@ import type { IAllRacesPagePresenter, + AllRacesPageResultDTO, AllRacesPageViewModel, } from '@gridpilot/racing/application/presenters/IAllRacesPagePresenter'; export class AllRacesPagePresenter implements IAllRacesPagePresenter { private viewModel: AllRacesPageViewModel | null = null; - present(viewModel: AllRacesPageViewModel): void { - this.viewModel = viewModel; + reset(): void { + this.viewModel = null; + } + + present(dto: AllRacesPageResultDTO): void { + this.viewModel = dto; } getViewModel(): AllRacesPageViewModel | null { diff --git a/apps/website/lib/presenters/DashboardOverviewPresenter.ts b/apps/website/lib/presenters/DashboardOverviewPresenter.ts index 56d686af9..7c3ff4cf2 100644 --- a/apps/website/lib/presenters/DashboardOverviewPresenter.ts +++ b/apps/website/lib/presenters/DashboardOverviewPresenter.ts @@ -1,13 +1,18 @@ import type { IDashboardOverviewPresenter, + DashboardOverviewResultDTO, DashboardOverviewViewModel, } from '@gridpilot/racing/application/presenters/IDashboardOverviewPresenter'; export class DashboardOverviewPresenter implements IDashboardOverviewPresenter { private viewModel: DashboardOverviewViewModel | null = null; - present(viewModel: DashboardOverviewViewModel): void { - this.viewModel = viewModel; + reset(): void { + this.viewModel = null; + } + + present(dto: DashboardOverviewResultDTO): void { + this.viewModel = dto; } getViewModel(): DashboardOverviewViewModel | null { diff --git a/apps/website/lib/presenters/DriversLeaderboardPresenter.ts b/apps/website/lib/presenters/DriversLeaderboardPresenter.ts index e5ca40ee7..b3ffbbe3f 100644 --- a/apps/website/lib/presenters/DriversLeaderboardPresenter.ts +++ b/apps/website/lib/presenters/DriversLeaderboardPresenter.ts @@ -1,21 +1,20 @@ -import type { Driver } from '@gridpilot/racing/domain/entities/Driver'; -import type { SkillLevel } from '@gridpilot/racing/domain/services/SkillLevelService'; import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService'; import type { IDriversLeaderboardPresenter, DriverLeaderboardItemViewModel, DriversLeaderboardViewModel, + DriversLeaderboardResultDTO, } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter { private viewModel: DriversLeaderboardViewModel | null = null; - present( - drivers: Driver[], - rankings: Array<{ driverId: string; rating: number; overallRank: number }>, - stats: Record, - avatarUrls: Record - ): DriversLeaderboardViewModel { + reset(): void { + this.viewModel = null; + } + + present(input: DriversLeaderboardResultDTO): void { + const { drivers, rankings, stats, avatarUrls } = input; const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => { const driverStats = stats[driver.id]; const rating = driverStats?.rating ?? 0; @@ -68,14 +67,9 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter totalWins, activeCount, }; - - return this.viewModel; } - getViewModel(): DriversLeaderboardViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } + getViewModel(): DriversLeaderboardViewModel | null { return this.viewModel; } } \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueDriverSeasonStatsPresenter.ts b/apps/website/lib/presenters/LeagueDriverSeasonStatsPresenter.ts index b16d680fa..ee6f13594 100644 --- a/apps/website/lib/presenters/LeagueDriverSeasonStatsPresenter.ts +++ b/apps/website/lib/presenters/LeagueDriverSeasonStatsPresenter.ts @@ -2,23 +2,19 @@ import type { ILeagueDriverSeasonStatsPresenter, LeagueDriverSeasonStatsItemViewModel, LeagueDriverSeasonStatsViewModel, + LeagueDriverSeasonStatsResultDTO, } from '@gridpilot/racing/application/presenters/ILeagueDriverSeasonStatsPresenter'; export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter { private viewModel: LeagueDriverSeasonStatsViewModel | null = null; - present( - leagueId: string, - standings: Array<{ - driverId: string; - position: number; - points: number; - racesCompleted: number; - }>, - penalties: Map, - driverResults: Map>, - driverRatings: Map - ): LeagueDriverSeasonStatsViewModel { + reset(): void { + this.viewModel = null; + } + + present(dto: LeagueDriverSeasonStatsResultDTO): void { + const { leagueId, standings, penalties, driverResults, driverRatings } = dto; + const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => { const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 }; const totalPenaltyPoints = penalty.baseDelta; @@ -65,8 +61,6 @@ export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStat leagueId, stats, }; - - return this.viewModel; } getViewModel(): LeagueDriverSeasonStatsViewModel { diff --git a/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts b/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts index ad4fe6aec..fdf4319ef 100644 --- a/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts +++ b/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts @@ -10,6 +10,10 @@ import type { export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter { private viewModel: LeagueScoringConfigViewModel | null = null; + reset(): void { + this.viewModel = null; + } + present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel { const championships: LeagueScoringChampionshipViewModel[] = data.championships.map((champ) => this.mapChampionship(champ)); diff --git a/apps/website/lib/presenters/RaceDetailPresenter.ts b/apps/website/lib/presenters/RaceDetailPresenter.ts index cf3d12062..b8408886f 100644 --- a/apps/website/lib/presenters/RaceDetailPresenter.ts +++ b/apps/website/lib/presenters/RaceDetailPresenter.ts @@ -6,9 +6,13 @@ import type { export class RaceDetailPresenter implements IRaceDetailPresenter { private viewModel: RaceDetailViewModel | null = null; + reset(): void { + this.viewModel = null; + } + present(viewModel: RaceDetailViewModel): RaceDetailViewModel { this.viewModel = viewModel; - return this.viewModel; + return viewModel; } getViewModel(): RaceDetailViewModel | null { diff --git a/apps/website/lib/presenters/RaceRegistrationsPresenter.ts b/apps/website/lib/presenters/RaceRegistrationsPresenter.ts index d1ae6acad..9ddb06713 100644 --- a/apps/website/lib/presenters/RaceRegistrationsPresenter.ts +++ b/apps/website/lib/presenters/RaceRegistrationsPresenter.ts @@ -1,24 +1,25 @@ import type { IRaceRegistrationsPresenter, RaceRegistrationsViewModel, + RaceRegistrationsResultDTO, } from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter'; export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter { private viewModel: RaceRegistrationsViewModel | null = null; - present(registeredDriverIds: string[]): RaceRegistrationsViewModel { + reset(): void { + this.viewModel = null; + } + + present(input: RaceRegistrationsResultDTO): void { + const { registeredDriverIds } = input; this.viewModel = { registeredDriverIds, count: registeredDriverIds.length, }; - - return this.viewModel; } - getViewModel(): RaceRegistrationsViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } + getViewModel(): RaceRegistrationsViewModel | null { return this.viewModel; } } \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceResultsDetailPresenter.ts b/apps/website/lib/presenters/RaceResultsDetailPresenter.ts index 278b0235c..7085d4891 100644 --- a/apps/website/lib/presenters/RaceResultsDetailPresenter.ts +++ b/apps/website/lib/presenters/RaceResultsDetailPresenter.ts @@ -6,9 +6,12 @@ import type { export class RaceResultsDetailPresenter implements IRaceResultsDetailPresenter { private viewModel: RaceResultsDetailViewModel | null = null; - present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel { + reset(): void { + this.viewModel = null; + } + + present(viewModel: RaceResultsDetailViewModel): void { this.viewModel = viewModel; - return this.viewModel; } getViewModel(): RaceResultsDetailViewModel | null { diff --git a/apps/website/lib/presenters/RaceWithSOFPresenter.ts b/apps/website/lib/presenters/RaceWithSOFPresenter.ts index 5a961fd22..a0106eec8 100644 --- a/apps/website/lib/presenters/RaceWithSOFPresenter.ts +++ b/apps/website/lib/presenters/RaceWithSOFPresenter.ts @@ -1,49 +1,35 @@ import type { IRaceWithSOFPresenter, + RaceWithSOFResultDTO, RaceWithSOFViewModel, } from '@gridpilot/racing/application/presenters/IRaceWithSOFPresenter'; export class RaceWithSOFPresenter implements IRaceWithSOFPresenter { private viewModel: RaceWithSOFViewModel | null = null; - present( - raceId: string, - leagueId: string, - scheduledAt: Date, - track: string, - trackId: string, - car: string, - carId: string, - sessionType: string, - status: string, - strengthOfField: number | null, - registeredCount: number, - maxParticipants: number, - participantCount: number - ): RaceWithSOFViewModel { + present(dto: RaceWithSOFResultDTO): void { this.viewModel = { - id: raceId, - leagueId, - scheduledAt: scheduledAt.toISOString(), - track, - trackId, - car, - carId, - sessionType, - status, - strengthOfField, - registeredCount, - maxParticipants, - participantCount, + id: dto.raceId, + leagueId: dto.leagueId, + scheduledAt: dto.scheduledAt.toISOString(), + track: dto.track, + trackId: dto.trackId, + car: dto.car, + carId: dto.carId, + sessionType: dto.sessionType, + status: dto.status, + strengthOfField: dto.strengthOfField, + registeredCount: dto.registeredCount, + maxParticipants: dto.maxParticipants, + participantCount: dto.participantCount, }; + } + getViewModel(): RaceWithSOFViewModel | null { return this.viewModel; } - getViewModel(): RaceWithSOFViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } - return this.viewModel; + reset(): void { + this.viewModel = null; } } \ No newline at end of file diff --git a/apps/website/lib/presenters/RacesPagePresenter.ts b/apps/website/lib/presenters/RacesPagePresenter.ts index a2efe5dc6..02c7a804f 100644 --- a/apps/website/lib/presenters/RacesPagePresenter.ts +++ b/apps/website/lib/presenters/RacesPagePresenter.ts @@ -2,26 +2,18 @@ import type { IRacesPagePresenter, RacesPageViewModel, RaceListItemViewModel, + RacesPageResultDTO, } from '@gridpilot/racing/application/presenters/IRacesPagePresenter'; -interface RacesPageInput { - id: string; - track: string; - car: string; - scheduledAt: string | Date; - status: string; - leagueId: string; - leagueName: string; - strengthOfField: number | null; - isUpcoming: boolean; - isLive: boolean; - isPast: boolean; -} - export class RacesPagePresenter implements IRacesPagePresenter { private viewModel: RacesPageViewModel | null = null; - present(races: RacesPageInput[]): void { + reset(): void { + this.viewModel = null; + } + + present(input: RacesPageResultDTO): void { + const { races } = input; const now = new Date(); const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); diff --git a/apps/website/lib/presenters/SponsorDashboardPresenter.ts b/apps/website/lib/presenters/SponsorDashboardPresenter.ts index 63795659c..0b368881a 100644 --- a/apps/website/lib/presenters/SponsorDashboardPresenter.ts +++ b/apps/website/lib/presenters/SponsorDashboardPresenter.ts @@ -1,14 +1,25 @@ -import type { ISponsorDashboardPresenter } from '@gridpilot/racing/application/presenters/ISponsorDashboardPresenter'; +import type { + ISponsorDashboardPresenter, + SponsorDashboardViewModel, +} from '@gridpilot/racing/application/presenters/ISponsorDashboardPresenter'; import type { SponsorDashboardDTO } from '@gridpilot/racing/application/use-cases/GetSponsorDashboardUseCase'; export class SponsorDashboardPresenter implements ISponsorDashboardPresenter { - private data: SponsorDashboardDTO | null = null; + private viewModel: SponsorDashboardViewModel = null; + + reset(): void { + this.viewModel = null; + } present(data: SponsorDashboardDTO | null): void { - this.data = data; + this.viewModel = data; + } + + getViewModel(): SponsorDashboardViewModel { + return this.viewModel; } getData(): SponsorDashboardDTO | null { - return this.data; + return this.viewModel; } } \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts b/apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts index d889928c5..fb80aa634 100644 --- a/apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts +++ b/apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts @@ -1,14 +1,25 @@ -import type { ISponsorSponsorshipsPresenter } from '@gridpilot/racing/application/presenters/ISponsorSponsorshipsPresenter'; +import type { + ISponsorSponsorshipsPresenter, + SponsorSponsorshipsViewModel, +} from '@gridpilot/racing/application/presenters/ISponsorSponsorshipsPresenter'; import type { SponsorSponsorshipsDTO } from '@gridpilot/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; export class SponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter { - private data: SponsorSponsorshipsDTO | null = null; + private viewModel: SponsorSponsorshipsViewModel = null; + + reset(): void { + this.viewModel = null; + } present(data: SponsorSponsorshipsDTO | null): void { - this.data = data; + this.viewModel = data; + } + + getViewModel(): SponsorSponsorshipsViewModel { + return this.viewModel; } getData(): SponsorSponsorshipsDTO | null { - return this.data; + return this.viewModel; } } \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamDetailsPresenter.ts b/apps/website/lib/presenters/TeamDetailsPresenter.ts index 223c271d4..4542b1386 100644 --- a/apps/website/lib/presenters/TeamDetailsPresenter.ts +++ b/apps/website/lib/presenters/TeamDetailsPresenter.ts @@ -1,18 +1,18 @@ -import type { Team } from '@gridpilot/racing/domain/entities/Team'; -import type { TeamMembership } from '@gridpilot/racing/domain/types/TeamMembership'; import type { ITeamDetailsPresenter, TeamDetailsViewModel, + TeamDetailsResultDTO, } from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter'; export class TeamDetailsPresenter implements ITeamDetailsPresenter { private viewModel: TeamDetailsViewModel | null = null; - present( - team: Team, - membership: TeamMembership | null, - driverId: string - ): TeamDetailsViewModel { + reset(): void { + this.viewModel = null; + } + + present(input: TeamDetailsResultDTO): void { + const { team, membership } = input; const canManage = membership?.role === 'owner' || membership?.role === 'manager'; const viewModel: TeamDetailsViewModel = { @@ -23,6 +23,7 @@ export class TeamDetailsPresenter implements ITeamDetailsPresenter { description: team.description, ownerId: team.ownerId, leagues: team.leagues, + createdAt: team.createdAt.toISOString(), }, membership: membership ? { @@ -35,14 +36,9 @@ export class TeamDetailsPresenter implements ITeamDetailsPresenter { }; this.viewModel = viewModel; - - return viewModel; } - getViewModel(): TeamDetailsViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } + getViewModel(): TeamDetailsViewModel | null { return this.viewModel; } } \ No newline at end of file diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index b5ef08f66..b80fc4e5f 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -1,21 +1,11 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noImplicitAny": true, - "noImplicitThis": true, - "alwaysStrict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "noEmit": true, - "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, + "baseUrl": ".", "jsx": "preserve", "incremental": true, "plugins": [ @@ -25,11 +15,19 @@ ], "paths": { "@/*": ["./*"], + "@/lib/*": ["./lib/*"], + "@/components/*": ["./components/*"], + "@/app/*": ["./app/*"], + "@gridpilot/identity": ["../../packages/identity/index.ts"], "@gridpilot/identity/*": ["../../packages/identity/*"], + "@gridpilot/racing": ["../../packages/racing/index.ts"], "@gridpilot/racing/*": ["../../packages/racing/*"], + "@gridpilot/social": ["../../packages/social/index.ts"], "@gridpilot/social/*": ["../../packages/social/*"], - "@gridpilot/testing-support": ["../../packages/testing-support"], - "@gridpilot/media": ["../../packages/media"], + "@gridpilot/testing-support": ["../../packages/testing-support/index.ts"], + "@gridpilot/testing-support/*": ["../../packages/testing-support/*"], + "@gridpilot/media": ["../../packages/media/index.ts"], + "@gridpilot/media/*": ["../../packages/media/*"], "@gridpilot/shared": ["../../packages/shared/index.ts"], "@gridpilot/shared/application": ["../../packages/shared/application"], "@gridpilot/shared/application/*": ["../../packages/shared/application/*"], @@ -40,5 +38,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", ".next"] } \ No newline at end of file diff --git a/apps/website/types/third-party-shims.d.ts b/apps/website/types/third-party-shims.d.ts new file mode 100644 index 000000000..0fb9c5c9f --- /dev/null +++ b/apps/website/types/third-party-shims.d.ts @@ -0,0 +1,4 @@ +/** + * Intentionally left blank. + * Third-party shims are now defined in env.d.ts. + */ \ No newline at end of file diff --git a/packages/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter.ts b/packages/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter.ts index e451bdfbe..e454924f0 100644 --- a/packages/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter.ts +++ b/packages/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter.ts @@ -2,6 +2,7 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig'; import { StepId } from '../../../../domain/value-objects/StepId'; import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort'; +import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig'; import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort'; import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator'; @@ -36,6 +37,34 @@ export class AutomationEngineAdapter implements AutomationEnginePort { private readonly sessionRepository: SessionRepositoryPort ) {} + private toStepConfig(config: HostedSessionConfig): Record { + const baseConfig: Record = { + sessionName: config.sessionName, + trackId: config.trackId, + carIds: [...config.carIds], + }; + + if (config.serverName !== undefined) baseConfig.serverName = config.serverName; + if (config.password !== undefined) baseConfig.password = config.password; + if (config.adminPassword !== undefined) baseConfig.adminPassword = config.adminPassword; + if (config.maxDrivers !== undefined) baseConfig.maxDrivers = config.maxDrivers; + if (config.carSearch !== undefined) baseConfig.carSearch = config.carSearch; + if (config.trackSearch !== undefined) baseConfig.trackSearch = config.trackSearch; + if (config.weatherType !== undefined) baseConfig.weatherType = config.weatherType; + if (config.timeOfDay !== undefined) baseConfig.timeOfDay = config.timeOfDay; + if (config.sessionDuration !== undefined) baseConfig.sessionDuration = config.sessionDuration; + if (config.practiceLength !== undefined) baseConfig.practiceLength = config.practiceLength; + if (config.qualifyingLength !== undefined) baseConfig.qualifyingLength = config.qualifyingLength; + if (config.warmupLength !== undefined) baseConfig.warmupLength = config.warmupLength; + if (config.raceLength !== undefined) baseConfig.raceLength = config.raceLength; + if (config.startType !== undefined) baseConfig.startType = config.startType; + if (config.restarts !== undefined) baseConfig.restarts = config.restarts; + if (config.damageModel !== undefined) baseConfig.damageModel = config.damageModel; + if (config.trackState !== undefined) baseConfig.trackState = config.trackState; + + return baseConfig; + } + async validateConfiguration(config: HostedSessionConfig): Promise { if (!config.sessionName || config.sessionName.trim() === '') { return { isValid: false, error: 'Session name is required' }; @@ -89,7 +118,7 @@ export class AutomationEngineAdapter implements AutomationEnginePort { // Execute current step using the browser automation if (this.browserAutomation.executeStep) { - const result = await this.browserAutomation.executeStep(currentStep, config as unknown as Record); + const result = await this.browserAutomation.executeStep(currentStep, this.toStepConfig(config)); if (!result.success) { const stepDescription = StepTransitionValidator.getStepDescription(currentStep); const errorMessage = `Step ${currentStep.value} (${stepDescription}) failed: ${result.error}`; @@ -117,7 +146,7 @@ export class AutomationEngineAdapter implements AutomationEnginePort { if (nextStep.isFinalStep()) { // Execute final step handler if (this.browserAutomation.executeStep) { - const result = await this.browserAutomation.executeStep(nextStep, config as unknown as Record); + const result = await this.browserAutomation.executeStep(nextStep, this.toStepConfig(config)); if (!result.success) { const stepDescription = StepTransitionValidator.getStepDescription(nextStep); const errorMessage = `Step ${nextStep.value} (${stepDescription}) failed: ${result.error}`; diff --git a/packages/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter.ts b/packages/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter.ts index 2a24efdc1..9eb9ca0c6 100644 --- a/packages/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter.ts +++ b/packages/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter.ts @@ -2,6 +2,7 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig'; import { StepId } from '../../../../domain/value-objects/StepId'; import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort'; +import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig'; import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort'; import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator'; @@ -19,6 +20,34 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort { private readonly sessionRepository: SessionRepositoryPort ) {} + private toStepConfig(config: HostedSessionConfig): Record { + const baseConfig: Record = { + sessionName: config.sessionName, + trackId: config.trackId, + carIds: [...config.carIds], + }; + + if (config.serverName !== undefined) baseConfig.serverName = config.serverName; + if (config.password !== undefined) baseConfig.password = config.password; + if (config.adminPassword !== undefined) baseConfig.adminPassword = config.adminPassword; + if (config.maxDrivers !== undefined) baseConfig.maxDrivers = config.maxDrivers; + if (config.carSearch !== undefined) baseConfig.carSearch = config.carSearch; + if (config.trackSearch !== undefined) baseConfig.trackSearch = config.trackSearch; + if (config.weatherType !== undefined) baseConfig.weatherType = config.weatherType; + if (config.timeOfDay !== undefined) baseConfig.timeOfDay = config.timeOfDay; + if (config.sessionDuration !== undefined) baseConfig.sessionDuration = config.sessionDuration; + if (config.practiceLength !== undefined) baseConfig.practiceLength = config.practiceLength; + if (config.qualifyingLength !== undefined) baseConfig.qualifyingLength = config.qualifyingLength; + if (config.warmupLength !== undefined) baseConfig.warmupLength = config.warmupLength; + if (config.raceLength !== undefined) baseConfig.raceLength = config.raceLength; + if (config.startType !== undefined) baseConfig.startType = config.startType; + if (config.restarts !== undefined) baseConfig.restarts = config.restarts; + if (config.damageModel !== undefined) baseConfig.damageModel = config.damageModel; + if (config.trackState !== undefined) baseConfig.trackState = config.trackState; + + return baseConfig; + } + async validateConfiguration(config: HostedSessionConfig): Promise { if (!config.sessionName || config.sessionName.trim() === '') { return { isValid: false, error: 'Session name is required' }; @@ -74,7 +103,7 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort { if (this.browserAutomation.executeStep) { const result = await this.browserAutomation.executeStep( currentStep, - config as unknown as Record, + this.toStepConfig(config), ); if (!result.success) { const stepDescription = StepTransitionValidator.getStepDescription(currentStep); @@ -105,7 +134,7 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort { if (this.browserAutomation.executeStep) { const result = await this.browserAutomation.executeStep( nextStep, - config as unknown as Record, + this.toStepConfig(config), ); if (!result.success) { const stepDescription = StepTransitionValidator.getStepDescription(nextStep); diff --git a/packages/automation/tsconfig.json b/packages/automation/tsconfig.json index 8c986bc80..56f2c4d42 100644 --- a/packages/automation/tsconfig.json +++ b/packages/automation/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "..", "outDir": "dist", diff --git a/packages/identity/tsconfig.json b/packages/identity/tsconfig.json index b7aa6ee1e..f5824790a 100644 --- a/packages/identity/tsconfig.json +++ b/packages/identity/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", "declaration": true, diff --git a/packages/media/tsconfig.json b/packages/media/tsconfig.json index 5558a6a0b..86e44fbd0 100644 --- a/packages/media/tsconfig.json +++ b/packages/media/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": ".", "outDir": "dist", diff --git a/packages/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts b/packages/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts index bddda7ac7..e0aba241e 100644 --- a/packages/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts +++ b/packages/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter.ts @@ -3,13 +3,14 @@ import type { Season } from '../../domain/entities/Season'; import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; import type { Game } from '../../domain/entities/Game'; import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider'; +import type { Presenter } from '@gridpilot/shared/presentation'; export interface LeagueSummaryViewModel { id: string; name: string; description: string; ownerId: string; - createdAt: Date; + createdAt: string; maxDrivers: number; usedDriverSlots: number; maxTeams?: number; @@ -20,7 +21,7 @@ export interface LeagueSummaryViewModel { scoring?: { gameId: string; gameName: string; - primaryChampionshipType: string; + primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy'; scoringPresetId: string; scoringPresetName: string; dropPolicySummary: string; @@ -42,6 +43,5 @@ export interface LeagueEnrichedData { preset?: LeagueScoringPresetDTO; } -export interface IAllLeaguesWithCapacityAndScoringPresenter { - present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel; -} \ No newline at end of file +export interface IAllLeaguesWithCapacityAndScoringPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts b/packages/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts index a8d27fed5..7c340e368 100644 --- a/packages/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts +++ b/packages/racing/application/presenters/IAllLeaguesWithCapacityPresenter.ts @@ -1,4 +1,5 @@ import type { League } from '../../domain/entities/League'; +import type { Presenter } from '@gridpilot/shared/presentation'; export interface LeagueWithCapacityViewModel { id: string; @@ -24,9 +25,10 @@ export interface AllLeaguesWithCapacityViewModel { totalCount: number; } -export interface IAllLeaguesWithCapacityPresenter { - present( - leagues: League[], - memberCounts: Map - ): AllLeaguesWithCapacityViewModel; -} \ No newline at end of file +export interface AllLeaguesWithCapacityResultDTO { + leagues: League[]; + memberCounts: Map; +} + +export interface IAllLeaguesWithCapacityPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IAllRacesPagePresenter.ts b/packages/racing/application/presenters/IAllRacesPagePresenter.ts index 6fec22a80..ccb16c52d 100644 --- a/packages/racing/application/presenters/IAllRacesPagePresenter.ts +++ b/packages/racing/application/presenters/IAllRacesPagePresenter.ts @@ -1,3 +1,5 @@ +import type { Presenter } from '@gridpilot/shared/presentation'; + export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'; export interface AllRacesListItemViewModel { @@ -21,7 +23,7 @@ export interface AllRacesPageViewModel { filters: AllRacesFilterOptionsViewModel; } -export interface IAllRacesPagePresenter { - present(viewModel: AllRacesPageViewModel): void; - getViewModel(): AllRacesPageViewModel | null; -} \ No newline at end of file +export type AllRacesPageResultDTO = AllRacesPageViewModel; + +export interface IAllRacesPagePresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IAllTeamsPresenter.ts b/packages/racing/application/presenters/IAllTeamsPresenter.ts index 303584f5e..0775c92b3 100644 --- a/packages/racing/application/presenters/IAllTeamsPresenter.ts +++ b/packages/racing/application/presenters/IAllTeamsPresenter.ts @@ -1,4 +1,3 @@ -import type { Team } from '../../domain/entities/Team'; import type { Presenter } from '@gridpilot/shared/presentation'; export interface TeamListItemViewModel { @@ -19,7 +18,16 @@ export interface AllTeamsViewModel { } export interface AllTeamsResultDTO { - teams: Array; + teams: Array<{ + id: string; + name: string; + tag: string; + description: string; + ownerId: string; + leagues: string[]; + createdAt: Date; + memberCount: number; + }>; } export interface IAllTeamsPresenter diff --git a/packages/racing/application/presenters/IDashboardOverviewPresenter.ts b/packages/racing/application/presenters/IDashboardOverviewPresenter.ts index 986c36ef9..37b0e3f02 100644 --- a/packages/racing/application/presenters/IDashboardOverviewPresenter.ts +++ b/packages/racing/application/presenters/IDashboardOverviewPresenter.ts @@ -1,3 +1,5 @@ +import type { Presenter } from '@gridpilot/shared/presentation'; + export interface DashboardDriverSummaryViewModel { id: string; name: string; @@ -82,7 +84,7 @@ export interface DashboardOverviewViewModel { friends: DashboardFriendSummaryViewModel[]; } -export interface IDashboardOverviewPresenter { - present(viewModel: DashboardOverviewViewModel): void; - getViewModel(): DashboardOverviewViewModel | null; -} \ No newline at end of file +export type DashboardOverviewResultDTO = DashboardOverviewViewModel; + +export interface IDashboardOverviewPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IDriversLeaderboardPresenter.ts b/packages/racing/application/presenters/IDriversLeaderboardPresenter.ts index bd97be9be..78607f319 100644 --- a/packages/racing/application/presenters/IDriversLeaderboardPresenter.ts +++ b/packages/racing/application/presenters/IDriversLeaderboardPresenter.ts @@ -1,5 +1,6 @@ import type { Driver } from '../../domain/entities/Driver'; import type { SkillLevel } from '../../domain/services/SkillLevelService'; +import type { Presenter } from '@gridpilot/shared/presentation'; export type { SkillLevel }; @@ -24,13 +25,21 @@ export interface DriversLeaderboardViewModel { activeCount: number; } -export interface IDriversLeaderboardPresenter { - present( - drivers: Driver[], - rankings: Array<{ driverId: string; rating: number; overallRank: number }>, - stats: Record, - avatarUrls: Record - ): DriversLeaderboardViewModel; +export interface DriversLeaderboardResultDTO { + drivers: Driver[]; + rankings: Array<{ driverId: string; rating: number; overallRank: number | null }>; + stats: Record< + string, + { + rating: number; + wins: number; + podiums: number; + totalRaces: number; + overallRank: number | null; + } + >; + avatarUrls: Record; +} - getViewModel(): DriversLeaderboardViewModel; -} \ No newline at end of file +export interface IDriversLeaderboardPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts b/packages/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts index d0ce63b36..e2bf28e46 100644 --- a/packages/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts +++ b/packages/racing/application/presenters/ILeagueDriverSeasonStatsPresenter.ts @@ -1,3 +1,5 @@ +import type { Presenter } from '@gridpilot/shared/presentation'; + export interface LeagueDriverSeasonStatsItemViewModel { leagueId: string; driverId: string; @@ -24,18 +26,18 @@ export interface LeagueDriverSeasonStatsViewModel { stats: LeagueDriverSeasonStatsItemViewModel[]; } -export interface ILeagueDriverSeasonStatsPresenter { - present( - leagueId: string, - standings: Array<{ - driverId: string; - position: number; - points: number; - racesCompleted: number; - }>, - penalties: Map, - driverResults: Map>, - driverRatings: Map - ): LeagueDriverSeasonStatsViewModel; - getViewModel(): LeagueDriverSeasonStatsViewModel; -} \ No newline at end of file +export interface LeagueDriverSeasonStatsResultDTO { + leagueId: string; + standings: Array<{ + driverId: string; + position: number; + points: number; + racesCompleted: number; + }>; + penalties: Map; + driverResults: Map>; + driverRatings: Map; +} + +export interface ILeagueDriverSeasonStatsPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/ILeagueScoringConfigPresenter.ts b/packages/racing/application/presenters/ILeagueScoringConfigPresenter.ts index 248412f8b..905e5aacc 100644 --- a/packages/racing/application/presenters/ILeagueScoringConfigPresenter.ts +++ b/packages/racing/application/presenters/ILeagueScoringConfigPresenter.ts @@ -1,5 +1,6 @@ import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig'; import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider'; +import type { Presenter } from '@gridpilot/shared/presentation'; export interface LeagueScoringChampionshipViewModel { id: string; @@ -32,7 +33,5 @@ export interface LeagueScoringConfigData { championships: ChampionshipConfig[]; } -export interface ILeagueScoringConfigPresenter { - present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel; - getViewModel(): LeagueScoringConfigViewModel; -} \ No newline at end of file +export interface ILeagueScoringConfigPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IRaceDetailPresenter.ts b/packages/racing/application/presenters/IRaceDetailPresenter.ts index 51217e50c..5a827e302 100644 --- a/packages/racing/application/presenters/IRaceDetailPresenter.ts +++ b/packages/racing/application/presenters/IRaceDetailPresenter.ts @@ -1,4 +1,5 @@ import type { SessionType, RaceStatus } from '../../domain/entities/Race'; +import type { Presenter } from '@gridpilot/shared/presentation'; export interface RaceDetailEntryViewModel { id: string; @@ -55,7 +56,5 @@ export interface RaceDetailViewModel { error?: string; } -export interface IRaceDetailPresenter { - present(viewModel: RaceDetailViewModel): RaceDetailViewModel; - getViewModel(): RaceDetailViewModel | null; -} \ No newline at end of file +export interface IRaceDetailPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IRacePenaltiesPresenter.ts b/packages/racing/application/presenters/IRacePenaltiesPresenter.ts index 2d70c0115..5e818b28b 100644 --- a/packages/racing/application/presenters/IRacePenaltiesPresenter.ts +++ b/packages/racing/application/presenters/IRacePenaltiesPresenter.ts @@ -1,4 +1,4 @@ -import type { PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty'; +import type { Penalty, PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty'; import type { Presenter } from '@gridpilot/shared/presentation/Presenter'; export interface RacePenaltyViewModel { @@ -24,21 +24,7 @@ export interface RacePenaltiesViewModel { } export interface RacePenaltiesResultDTO { - penalties: Array<{ - id: string; - raceId: string; - driverId: string; - type: PenaltyType; - value?: number; - reason: string; - protestId?: string; - issuedBy: string; - status: PenaltyStatus; - issuedAt: Date; - appliedAt?: Date; - notes?: string; - getDescription(): string; - }>; + penalties: Penalty[]; driverMap: Map; } diff --git a/packages/racing/application/presenters/IRaceProtestsPresenter.ts b/packages/racing/application/presenters/IRaceProtestsPresenter.ts index 379c7f905..27b77af4f 100644 --- a/packages/racing/application/presenters/IRaceProtestsPresenter.ts +++ b/packages/racing/application/presenters/IRaceProtestsPresenter.ts @@ -1,4 +1,4 @@ -import type { ProtestStatus, ProtestIncident } from '../../domain/entities/Protest'; +import type { Protest, ProtestStatus, ProtestIncident } from '../../domain/entities/Protest'; import type { Presenter } from '@gridpilot/shared/presentation/Presenter'; export interface RaceProtestViewModel { @@ -24,20 +24,7 @@ export interface RaceProtestsViewModel { } export interface RaceProtestsResultDTO { - protests: Array<{ - id: string; - raceId: string; - protestingDriverId: string; - accusedDriverId: string; - incident: ProtestIncident; - comment?: string; - proofVideoUrl?: string; - status: ProtestStatus; - reviewedBy?: string; - decisionNotes?: string; - filedAt: Date; - reviewedAt?: Date; - }>; + protests: Protest[]; driverMap: Map; } diff --git a/packages/racing/application/presenters/IRaceRegistrationsPresenter.ts b/packages/racing/application/presenters/IRaceRegistrationsPresenter.ts index 1bfbd4b1e..8c01b6392 100644 --- a/packages/racing/application/presenters/IRaceRegistrationsPresenter.ts +++ b/packages/racing/application/presenters/IRaceRegistrationsPresenter.ts @@ -1,9 +1,13 @@ +import type { Presenter } from '@gridpilot/shared/presentation'; + export interface RaceRegistrationsViewModel { registeredDriverIds: string[]; count: number; } -export interface IRaceRegistrationsPresenter { - present(registeredDriverIds: string[]): RaceRegistrationsViewModel; - getViewModel(): RaceRegistrationsViewModel; -} \ No newline at end of file +export interface RaceRegistrationsResultDTO { + registeredDriverIds: string[]; +} + +export interface IRaceRegistrationsPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IRaceResultsDetailPresenter.ts b/packages/racing/application/presenters/IRaceResultsDetailPresenter.ts index 9e0a55b34..bd49a228f 100644 --- a/packages/racing/application/presenters/IRaceResultsDetailPresenter.ts +++ b/packages/racing/application/presenters/IRaceResultsDetailPresenter.ts @@ -2,6 +2,7 @@ import type { RaceStatus } from '../../domain/entities/Race'; import type { Result } from '../../domain/entities/Result'; import type { Driver } from '../../domain/entities/Driver'; import type { PenaltyType } from '../../domain/entities/Penalty'; +import type { Presenter } from '@gridpilot/shared/presentation'; export interface RaceResultsHeaderViewModel { id: string; @@ -28,13 +29,11 @@ export interface RaceResultsDetailViewModel { results: Result[]; drivers: Driver[]; penalties: RaceResultsPenaltySummaryViewModel[]; - pointsSystem: Record; + pointsSystem?: Record; fastestLapTime?: number; currentDriverId?: string; error?: string; } -export interface IRaceResultsDetailPresenter { - present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel; - getViewModel(): RaceResultsDetailViewModel | null; -} \ No newline at end of file +export interface IRaceResultsDetailPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IRaceWithSOFPresenter.ts b/packages/racing/application/presenters/IRaceWithSOFPresenter.ts index 5d9d1c017..2a0e91484 100644 --- a/packages/racing/application/presenters/IRaceWithSOFPresenter.ts +++ b/packages/racing/application/presenters/IRaceWithSOFPresenter.ts @@ -1,3 +1,5 @@ +import type { Presenter } from '@gridpilot/shared/presentation'; + export interface RaceWithSOFViewModel { id: string; leagueId: string; @@ -14,21 +16,21 @@ export interface RaceWithSOFViewModel { participantCount: number; } -export interface IRaceWithSOFPresenter { - present( - raceId: string, - leagueId: string, - scheduledAt: Date, - track: string, - trackId: string, - car: string, - carId: string, - sessionType: string, - status: string, - strengthOfField: number | null, - registeredCount: number, - maxParticipants: number, - participantCount: number - ): RaceWithSOFViewModel; - getViewModel(): RaceWithSOFViewModel; -} \ No newline at end of file +export interface RaceWithSOFResultDTO { + raceId: string; + leagueId: string; + scheduledAt: Date; + track: string; + trackId: string; + car: string; + carId: string; + sessionType: string; + status: string; + strengthOfField: number | null; + registeredCount: number; + maxParticipants: number; + participantCount: number; +} + +export interface IRaceWithSOFPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/IRacesPagePresenter.ts b/packages/racing/application/presenters/IRacesPagePresenter.ts index db7db156c..33eaed22f 100644 --- a/packages/racing/application/presenters/IRacesPagePresenter.ts +++ b/packages/racing/application/presenters/IRacesPagePresenter.ts @@ -1,3 +1,5 @@ +import type { Presenter } from '@gridpilot/shared/presentation'; + export interface RaceListItemViewModel { id: string; track: string; @@ -25,7 +27,9 @@ export interface RacesPageViewModel { recentResults: RaceListItemViewModel[]; } -export interface IRacesPagePresenter { - present(races: any[]): void; - getViewModel(): RacesPageViewModel; -} \ No newline at end of file +export interface RacesPageResultDTO { + races: any[]; +} + +export interface IRacesPagePresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/ISponsorDashboardPresenter.ts b/packages/racing/application/presenters/ISponsorDashboardPresenter.ts index 2a95430c8..2c86f0707 100644 --- a/packages/racing/application/presenters/ISponsorDashboardPresenter.ts +++ b/packages/racing/application/presenters/ISponsorDashboardPresenter.ts @@ -1,5 +1,7 @@ -import type { SponsoredLeagueDTO, SponsorDashboardDTO } from '../use-cases/GetSponsorDashboardUseCase'; +import type { SponsorDashboardDTO } from '../use-cases/GetSponsorDashboardUseCase'; +import type { Presenter } from '@gridpilot/shared/presentation'; -export interface ISponsorDashboardPresenter { - present(data: SponsorDashboardDTO | null): void; -} \ No newline at end of file +export type SponsorDashboardViewModel = SponsorDashboardDTO | null; + +export interface ISponsorDashboardPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/ISponsorSponsorshipsPresenter.ts b/packages/racing/application/presenters/ISponsorSponsorshipsPresenter.ts index 4c1fb4095..fa0800e61 100644 --- a/packages/racing/application/presenters/ISponsorSponsorshipsPresenter.ts +++ b/packages/racing/application/presenters/ISponsorSponsorshipsPresenter.ts @@ -1,5 +1,7 @@ import type { SponsorSponsorshipsDTO } from '../use-cases/GetSponsorSponsorshipsUseCase'; +import type { Presenter } from '@gridpilot/shared/presentation'; -export interface ISponsorSponsorshipsPresenter { - present(data: SponsorSponsorshipsDTO | null): void; -} \ No newline at end of file +export type SponsorSponsorshipsViewModel = SponsorSponsorshipsDTO | null; + +export interface ISponsorSponsorshipsPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/presenters/ITeamDetailsPresenter.ts b/packages/racing/application/presenters/ITeamDetailsPresenter.ts index 6aad771ad..44364a5e7 100644 --- a/packages/racing/application/presenters/ITeamDetailsPresenter.ts +++ b/packages/racing/application/presenters/ITeamDetailsPresenter.ts @@ -1,5 +1,6 @@ import type { Team } from '../../domain/entities/Team'; import type { TeamMembership } from '../../domain/types/TeamMembership'; +import type { Presenter } from '@gridpilot/shared/presentation'; export interface TeamDetailsViewModel { team: { @@ -9,9 +10,7 @@ export interface TeamDetailsViewModel { description: string; ownerId: string; leagues: string[]; - specialization?: 'endurance' | 'sprint' | 'mixed'; - region?: string; - languages?: string[]; + createdAt: string; }; membership: { role: 'owner' | 'manager' | 'member'; @@ -21,10 +20,11 @@ export interface TeamDetailsViewModel { canManage: boolean; } -export interface ITeamDetailsPresenter { - present( - team: Team, - membership: TeamMembership | null, - driverId: string - ): TeamDetailsViewModel; -} \ No newline at end of file +export interface TeamDetailsResultDTO { + team: Team; + membership: TeamMembership | null; + driverId: string; +} + +export interface ITeamDetailsPresenter + extends Presenter {} \ No newline at end of file diff --git a/packages/racing/application/use-cases/CreateTeamUseCase.ts b/packages/racing/application/use-cases/CreateTeamUseCase.ts index 7e7841f96..0d6d4d451 100644 --- a/packages/racing/application/use-cases/CreateTeamUseCase.ts +++ b/packages/racing/application/use-cases/CreateTeamUseCase.ts @@ -27,15 +27,14 @@ export class CreateTeamUseCase { throw new Error('Driver already belongs to a team'); } - const team: Team = { + const team = Team.create({ id: `team-${Date.now()}`, name, tag, description, ownerId, leagues, - createdAt: new Date(), - }; + }); const createdTeam = await this.teamRepository.create(team); diff --git a/packages/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts b/packages/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts index fcacdcd46..be9e4ae45 100644 --- a/packages/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts +++ b/packages/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase.ts @@ -4,15 +4,25 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider'; -import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { + AllLeaguesWithCapacityAndScoringViewModel, + IAllLeaguesWithCapacityAndScoringPresenter, + LeagueEnrichedData, +} from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; /** * Use Case for retrieving all leagues with capacity and scoring information. * Orchestrates domain logic and delegates presentation to the presenter. */ export class GetAllLeaguesWithCapacityAndScoringUseCase - implements AsyncUseCase + implements + UseCase< + void, + LeagueEnrichedData[], + AllLeaguesWithCapacityAndScoringViewModel, + IAllLeaguesWithCapacityAndScoringPresenter + > { constructor( private readonly leagueRepository: ILeagueRepository, @@ -21,10 +31,14 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly gameRepository: IGameRepository, private readonly presetProvider: LeagueScoringPresetProvider, - public readonly presenter: IAllLeaguesWithCapacityAndScoringPresenter, ) {} - async execute(): Promise { + async execute( + _input: void, + presenter: IAllLeaguesWithCapacityAndScoringPresenter, + ): Promise { + presenter.reset(); + const leagues = await this.leagueRepository.findAll(); const enrichedLeagues: LeagueEnrichedData[] = []; @@ -42,18 +56,22 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase ).length; const seasons = await this.seasonRepository.findByLeagueId(league.id); - const activeSeason = seasons && seasons.length > 0 - ? seasons.find((s) => s.status === 'active') ?? seasons[0] - : undefined; + const activeSeason = + seasons && seasons.length > 0 + ? seasons.find((s) => s.status === 'active') ?? seasons[0] + : undefined; let scoringConfig: LeagueEnrichedData['scoringConfig']; let game: LeagueEnrichedData['game']; let preset: LeagueEnrichedData['preset']; if (activeSeason) { - scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id); + const scoringConfigResult = + await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id); + scoringConfig = scoringConfigResult ?? undefined; if (scoringConfig) { - game = await this.gameRepository.findById(activeSeason.gameId); + const gameResult = await this.gameRepository.findById(activeSeason.gameId); + game = gameResult ?? undefined; const presetId = scoringConfig.scoringPresetId; if (presetId) { preset = this.presetProvider.getPresetById(presetId); @@ -64,13 +82,13 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase enrichedLeagues.push({ league, usedDriverSlots, - season: activeSeason, - ...(scoringConfig ?? undefined ? { scoringConfig } : {}), - ...(game ?? undefined ? { game } : {}), - ...(preset ?? undefined ? { preset } : {}), + ...(activeSeason ? { season: activeSeason } : {}), + ...(scoringConfig ? { scoringConfig } : {}), + ...(game ? { game } : {}), + ...(preset ? { preset } : {}), }); } - this.presenter.present(enrichedLeagues); + presenter.present(enrichedLeagues); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts b/packages/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts index 63a0f8b5a..391dc479b 100644 --- a/packages/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts +++ b/packages/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts @@ -1,22 +1,30 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; -import type { IAllLeaguesWithCapacityPresenter } from '../presenters/IAllLeaguesWithCapacityPresenter'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { + IAllLeaguesWithCapacityPresenter, + AllLeaguesWithCapacityResultDTO, + AllLeaguesWithCapacityViewModel, +} from '../presenters/IAllLeaguesWithCapacityPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; /** * Use Case for retrieving all leagues with capacity information. * Orchestrates domain logic and delegates presentation to the presenter. */ export class GetAllLeaguesWithCapacityUseCase - implements AsyncUseCase + implements UseCase { constructor( private readonly leagueRepository: ILeagueRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, - public readonly presenter: IAllLeaguesWithCapacityPresenter, ) {} - async execute(): Promise { + async execute( + _input: void, + presenter: IAllLeaguesWithCapacityPresenter, + ): Promise { + presenter.reset(); + const leagues = await this.leagueRepository.findAll(); const memberCounts = new Map(); @@ -36,6 +44,11 @@ export class GetAllLeaguesWithCapacityUseCase memberCounts.set(league.id, usedSlots); } - this.presenter.present(leagues, memberCounts); + const dto: AllLeaguesWithCapacityResultDTO = { + leagues, + memberCounts, + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts b/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts index ff43dcb1a..91e5de191 100644 --- a/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts +++ b/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts @@ -2,21 +2,21 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository' import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { IAllRacesPagePresenter, + AllRacesPageResultDTO, AllRacesPageViewModel, AllRacesListItemViewModel, AllRacesFilterOptionsViewModel, } from '../presenters/IAllRacesPagePresenter'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { UseCase } from '@gridpilot/shared/application'; export class GetAllRacesPageDataUseCase - implements AsyncUseCase { + implements UseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, - public readonly presenter: IAllRacesPagePresenter, ) {} - async execute(): Promise { + async execute(_input: void, presenter: IAllRacesPagePresenter): Promise { const [allRaces, allLeagues] = await Promise.all([ this.raceRepository.findAll(), this.leagueRepository.findAll(), @@ -59,6 +59,7 @@ export class GetAllRacesPageDataUseCase filters, }; - this.presenter.present(viewModel); + presenter.reset(); + presenter.present(viewModel); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetAllTeamsUseCase.ts b/packages/racing/application/use-cases/GetAllTeamsUseCase.ts index 535b2ac56..e849e0f25 100644 --- a/packages/racing/application/use-cases/GetAllTeamsUseCase.ts +++ b/packages/racing/application/use-cases/GetAllTeamsUseCase.ts @@ -24,11 +24,17 @@ export class GetAllTeamsUseCase const teams = await this.teamRepository.findAll(); - const enrichedTeams: Array = await Promise.all( + const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all( teams.map(async (team) => { const memberCount = await this.teamMembershipRepository.countByTeamId(team.id); return { - ...team, + id: team.id, + name: team.name, + tag: team.tag, + description: team.description, + ownerId: team.ownerId, + leagues: [...team.leagues], + createdAt: team.createdAt, memberCount, }; }), diff --git a/packages/racing/application/use-cases/GetDashboardOverviewUseCase.ts b/packages/racing/application/use-cases/GetDashboardOverviewUseCase.ts index 3d61da1f0..11684221c 100644 --- a/packages/racing/application/use-cases/GetDashboardOverviewUseCase.ts +++ b/packages/racing/application/use-cases/GetDashboardOverviewUseCase.ts @@ -8,7 +8,6 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac import type { IImageServicePort } from '../ports/IImageServicePort'; import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { IDashboardOverviewPresenter, DashboardOverviewViewModel, @@ -34,8 +33,7 @@ export interface GetDashboardOverviewParams { driverId: string; } -export class GetDashboardOverviewUseCase - implements AsyncUseCase { +export class GetDashboardOverviewUseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly raceRepository: IRaceRepository, @@ -48,10 +46,9 @@ export class GetDashboardOverviewUseCase private readonly socialRepository: ISocialGraphRepository, private readonly imageService: IImageServicePort, private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null, - public readonly presenter: IDashboardOverviewPresenter, ) {} - async execute(params: GetDashboardOverviewParams): Promise { + async execute(params: GetDashboardOverviewParams, presenter: IDashboardOverviewPresenter): Promise { const { driverId } = params; const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([ @@ -137,7 +134,8 @@ export class GetDashboardOverviewUseCase friends: friendsSummary, }; - this.presenter.present(viewModel); + presenter.reset(); + presenter.present(viewModel); } private async getDriverLeagues(allLeagues: any[], driverId: string): Promise { diff --git a/packages/racing/application/use-cases/GetDriversLeaderboardUseCase.ts b/packages/racing/application/use-cases/GetDriversLeaderboardUseCase.ts index 4ce58c039..05985f583 100644 --- a/packages/racing/application/use-cases/GetDriversLeaderboardUseCase.ts +++ b/packages/racing/application/use-cases/GetDriversLeaderboardUseCase.ts @@ -2,30 +2,36 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit import type { IRankingService } from '../../domain/services/IRankingService'; import type { IDriverStatsService } from '../../domain/services/IDriverStatsService'; import type { IImageServicePort } from '../ports/IImageServicePort'; -import type { IDriversLeaderboardPresenter } from '../presenters/IDriversLeaderboardPresenter'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { + IDriversLeaderboardPresenter, + DriversLeaderboardResultDTO, + DriversLeaderboardViewModel, +} from '../presenters/IDriversLeaderboardPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; /** * Use Case for retrieving driver leaderboard data. * Orchestrates domain logic and delegates presentation to the presenter. */ export class GetDriversLeaderboardUseCase - implements AsyncUseCase { + implements UseCase +{ constructor( private readonly driverRepository: IDriverRepository, private readonly rankingService: IRankingService, private readonly driverStatsService: IDriverStatsService, private readonly imageService: IImageServicePort, - public readonly presenter: IDriversLeaderboardPresenter, ) {} - async execute(): Promise { + async execute(_input: void, presenter: IDriversLeaderboardPresenter): Promise { + presenter.reset(); + const drivers = await this.driverRepository.findAll(); const rankings = this.rankingService.getAllDriverRankings(); - - const stats: Record = {}; - const avatarUrls: Record = {}; - + + const stats: DriversLeaderboardResultDTO['stats'] = {}; + const avatarUrls: DriversLeaderboardResultDTO['avatarUrls'] = {}; + for (const driver of drivers) { const driverStats = this.driverStatsService.getDriverStats(driver.id); if (driverStats) { @@ -33,7 +39,14 @@ export class GetDriversLeaderboardUseCase } avatarUrls[driver.id] = this.imageService.getDriverAvatar(driver.id); } - - this.presenter.present(drivers, rankings, stats, avatarUrls); + + const dto: DriversLeaderboardResultDTO = { + drivers, + rankings, + stats, + avatarUrls, + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts b/packages/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts index 179ce600e..9170fad60 100644 --- a/packages/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts +++ b/packages/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts @@ -2,8 +2,12 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { + ILeagueDriverSeasonStatsPresenter, + LeagueDriverSeasonStatsResultDTO, + LeagueDriverSeasonStatsViewModel, +} from '../presenters/ILeagueDriverSeasonStatsPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; export interface DriverRatingPort { getRating(driverId: string): { rating: number | null; ratingChange: number | null }; @@ -18,17 +22,27 @@ export interface GetLeagueDriverSeasonStatsUseCaseParams { * Orchestrates domain logic and delegates presentation to the presenter. */ export class GetLeagueDriverSeasonStatsUseCase - implements AsyncUseCase { + implements + UseCase< + GetLeagueDriverSeasonStatsUseCaseParams, + LeagueDriverSeasonStatsResultDTO, + LeagueDriverSeasonStatsViewModel, + ILeagueDriverSeasonStatsPresenter + > +{ constructor( private readonly standingRepository: IStandingRepository, private readonly resultRepository: IResultRepository, private readonly penaltyRepository: IPenaltyRepository, private readonly raceRepository: IRaceRepository, private readonly driverRatingPort: DriverRatingPort, - public readonly presenter: ILeagueDriverSeasonStatsPresenter, ) {} - async execute(params: GetLeagueDriverSeasonStatsUseCaseParams): Promise { + async execute( + params: GetLeagueDriverSeasonStatsUseCaseParams, + presenter: ILeagueDriverSeasonStatsPresenter, + ): Promise { + presenter.reset(); const { leagueId } = params; // Get standings and races for the league @@ -70,16 +84,26 @@ export class GetLeagueDriverSeasonStatsUseCase // Collect driver results const driverResults = new Map>(); for (const standing of standings) { - const results = await this.resultRepository.findByDriverIdAndLeagueId(standing.driverId, leagueId); + const results = await this.resultRepository.findByDriverIdAndLeagueId( + standing.driverId, + leagueId, + ); driverResults.set(standing.driverId, results); } - this.presenter.present( + const dto: LeagueDriverSeasonStatsResultDTO = { leagueId, - standings, - penaltiesByDriver, + standings: standings.map(standing => ({ + driverId: standing.driverId, + position: standing.position, + points: standing.points, + racesCompleted: standing.racesCompleted, + })), + penalties: penaltiesByDriver, driverResults, - driverRatings - ); + driverRatings, + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetLeagueFullConfigUseCase.ts b/packages/racing/application/use-cases/GetLeagueFullConfigUseCase.ts index 231f50d9f..25735c60c 100644 --- a/packages/racing/application/use-cases/GetLeagueFullConfigUseCase.ts +++ b/packages/racing/application/use-cases/GetLeagueFullConfigUseCase.ts @@ -49,9 +49,9 @@ export class GetLeagueFullConfigUseCase const data: LeagueFullConfigData = { league, - activeSeason, - ...(scoringConfig ?? undefined ? { scoringConfig } : {}), - ...(game ?? undefined ? { game } : {}), + ...(activeSeason ? { activeSeason } : {}), + ...(scoringConfig ? { scoringConfig } : {}), + ...(game ? { game } : {}), }; presenter.reset(); diff --git a/packages/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts b/packages/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts index 4e3f732a6..07e3b7b06 100644 --- a/packages/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts +++ b/packages/racing/application/use-cases/GetLeagueScoringConfigUseCase.ts @@ -3,25 +3,29 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider'; -import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { + ILeagueScoringConfigPresenter, + LeagueScoringConfigData, + LeagueScoringConfigViewModel, +} from '../presenters/ILeagueScoringConfigPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; /** * Use Case for retrieving a league's scoring configuration for its active season. * Orchestrates domain logic and delegates presentation to the presenter. */ export class GetLeagueScoringConfigUseCase - implements AsyncUseCase<{ leagueId: string }, void> { + implements UseCase<{ leagueId: string }, LeagueScoringConfigData, LeagueScoringConfigViewModel, ILeagueScoringConfigPresenter> +{ constructor( private readonly leagueRepository: ILeagueRepository, private readonly seasonRepository: ISeasonRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly gameRepository: IGameRepository, private readonly presetProvider: LeagueScoringPresetProvider, - public readonly presenter: ILeagueScoringConfigPresenter, ) {} - async execute(params: { leagueId: string }): Promise { + async execute(params: { leagueId: string }, presenter: ILeagueScoringConfigPresenter): Promise { const { leagueId } = params; const league = await this.leagueRepository.findById(leagueId); @@ -65,6 +69,7 @@ export class GetLeagueScoringConfigUseCase championships: scoringConfig.championships, }; - this.presenter.present(data); + presenter.reset(); + presenter.present(data); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetRaceDetailUseCase.ts b/packages/racing/application/use-cases/GetRaceDetailUseCase.ts index 6bf2d989e..3739fa2af 100644 --- a/packages/racing/application/use-cases/GetRaceDetailUseCase.ts +++ b/packages/racing/application/use-cases/GetRaceDetailUseCase.ts @@ -14,6 +14,7 @@ import type { RaceDetailEntryViewModel, RaceDetailUserResultViewModel, } from '../presenters/IRaceDetailPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; /** * Use Case: GetRaceDetailUseCase @@ -30,7 +31,9 @@ export interface GetRaceDetailQueryParams { driverId: string; } -export class GetRaceDetailUseCase { +export class GetRaceDetailUseCase + implements UseCase +{ constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, @@ -40,10 +43,11 @@ export class GetRaceDetailUseCase { private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly driverRatingProvider: DriverRatingProvider, private readonly imageService: IImageServicePort, - public readonly presenter: IRaceDetailPresenter, ) {} - async execute(params: GetRaceDetailQueryParams): Promise { + async execute(params: GetRaceDetailQueryParams, presenter: IRaceDetailPresenter): Promise { + presenter.reset(); + const { raceId, driverId } = params; const race = await this.raceRepository.findById(raceId); @@ -59,7 +63,7 @@ export class GetRaceDetailUseCase { userResult: null, error: 'Race not found', }; - this.presenter.present(emptyViewModel); + presenter.present(emptyViewModel); return; } @@ -121,8 +125,8 @@ export class GetRaceDetailUseCase { sessionType: race.sessionType, status: race.status, strengthOfField: race.strengthOfField ?? null, - registeredCount: race.registeredCount, - maxParticipants: race.maxParticipants, + ...(race.registeredCount !== undefined ? { registeredCount: race.registeredCount } : {}), + ...(race.maxParticipants !== undefined ? { maxParticipants: race.maxParticipants } : {}), }; const leagueView: RaceDetailLeagueViewModel | null = league @@ -131,8 +135,12 @@ export class GetRaceDetailUseCase { name: league.name, description: league.description, settings: { - maxDrivers: league.settings.maxDrivers, - qualifyingFormat: league.settings.qualifyingFormat, + ...(league.settings.maxDrivers !== undefined + ? { maxDrivers: league.settings.maxDrivers } + : {}), + ...(league.settings.qualifyingFormat !== undefined + ? { qualifyingFormat: league.settings.qualifyingFormat } + : {}), }, } : null; @@ -148,7 +156,7 @@ export class GetRaceDetailUseCase { userResult: userResultView, }; - this.presenter.present(viewModel); + presenter.present(viewModel); } private calculateRatingChange(position: number): number { diff --git a/packages/racing/application/use-cases/GetRaceRegistrationsUseCase.ts b/packages/racing/application/use-cases/GetRaceRegistrationsUseCase.ts index 9f379948f..d9481361b 100644 --- a/packages/racing/application/use-cases/GetRaceRegistrationsUseCase.ts +++ b/packages/racing/application/use-cases/GetRaceRegistrationsUseCase.ts @@ -1,6 +1,11 @@ import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository'; import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO'; -import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistrationsPresenter'; +import type { + IRaceRegistrationsPresenter, + RaceRegistrationsResultDTO, + RaceRegistrationsViewModel, +} from '../presenters/IRaceRegistrationsPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; /** * Use Case: GetRaceRegistrationsUseCase @@ -8,15 +13,26 @@ import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistratio * Returns registered driver IDs for a race. * Orchestrates domain logic and delegates presentation to the presenter. */ -export class GetRaceRegistrationsUseCase { +export class GetRaceRegistrationsUseCase + implements UseCase +{ constructor( private readonly registrationRepository: IRaceRegistrationRepository, - public readonly presenter: IRaceRegistrationsPresenter, ) {} - async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise { + async execute( + params: GetRaceRegistrationsQueryParamsDTO, + presenter: IRaceRegistrationsPresenter, + ): Promise { + presenter.reset(); + const { raceId } = params; const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId); - this.presenter.present(registeredDriverIds); + + const dto: RaceRegistrationsResultDTO = { + registeredDriverIds, + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetRaceResultsDetailUseCase.ts b/packages/racing/application/use-cases/GetRaceResultsDetailUseCase.ts index ea9b0d33e..50a703540 100644 --- a/packages/racing/application/use-cases/GetRaceResultsDetailUseCase.ts +++ b/packages/racing/application/use-cases/GetRaceResultsDetailUseCase.ts @@ -8,6 +8,7 @@ import type { RaceResultsDetailViewModel, RaceResultsPenaltySummaryViewModel, } from '../presenters/IRaceResultsDetailPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; import type { League } from '../../domain/entities/League'; import type { Result } from '../../domain/entities/Result'; import type { Driver } from '../../domain/entities/Driver'; @@ -18,8 +19,8 @@ export interface GetRaceResultsDetailParams { driverId?: string; } -function buildPointsSystem(league: League | null): Record { - if (!league) return {}; +function buildPointsSystem(league: League | null): Record | undefined { + if (!league) return undefined; const pointsSystems: Record> = { 'f1-2024': { @@ -53,11 +54,17 @@ function buildPointsSystem(league: League | null): Record { }, }; - return ( - league.settings.customPoints || - pointsSystems[league.settings.pointsSystem] || - pointsSystems['f1-2024'] - ); + const customPoints = league.settings.customPoints; + if (customPoints) { + return customPoints; + } + + const preset = pointsSystems[league.settings.pointsSystem]; + if (preset) { + return preset; + } + + return pointsSystems['f1-2024']; } function getFastestLapTime(results: Result[]): number | undefined { @@ -73,17 +80,28 @@ function mapPenaltySummary(penalties: Penalty[]): RaceResultsPenaltySummaryViewM })); } -export class GetRaceResultsDetailUseCase { +export class GetRaceResultsDetailUseCase + implements + UseCase< + GetRaceResultsDetailParams, + RaceResultsDetailViewModel, + RaceResultsDetailViewModel, + IRaceResultsDetailPresenter + > +{ constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, private readonly resultRepository: IResultRepository, private readonly driverRepository: IDriverRepository, private readonly penaltyRepository: IPenaltyRepository, - public readonly presenter: IRaceResultsDetailPresenter, ) {} - async execute(params: GetRaceResultsDetailParams): Promise { + async execute( + params: GetRaceResultsDetailParams, + presenter: IRaceResultsDetailPresenter, + ): Promise { + presenter.reset(); const { raceId, driverId } = params; const race = await this.raceRepository.findById(raceId); @@ -95,11 +113,10 @@ export class GetRaceResultsDetailUseCase { results: [], drivers: [], penalties: [], - pointsSystem: {}, - currentDriverId: driverId, + ...(driverId ? { currentDriverId: driverId } : {}), error: 'Race not found', }; - this.presenter.present(errorViewModel); + presenter.present(errorViewModel); return; } @@ -111,12 +128,12 @@ export class GetRaceResultsDetailUseCase { ]); const effectiveCurrentDriverId = - driverId || (drivers.length > 0 ? drivers[0]!.id : undefined); + driverId ?? (drivers.length > 0 ? drivers[0]!.id : undefined); const pointsSystem = buildPointsSystem(league as League | null); const fastestLapTime = getFastestLapTime(results); const penaltySummary = mapPenaltySummary(penalties); - + const viewModel: RaceResultsDetailViewModel = { race: { id: race.id, @@ -134,11 +151,11 @@ export class GetRaceResultsDetailUseCase { results, drivers, penalties: penaltySummary, - pointsSystem, + ...(pointsSystem ? { pointsSystem } : {}), ...(fastestLapTime !== undefined ? { fastestLapTime } : {}), - currentDriverId: effectiveCurrentDriverId, + ...(effectiveCurrentDriverId ? { currentDriverId: effectiveCurrentDriverId } : {}), }; - this.presenter.present(viewModel); + presenter.present(viewModel); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetRaceWithSOFUseCase.ts b/packages/racing/application/use-cases/GetRaceWithSOFUseCase.ts index c6ad29713..4ef7cab1c 100644 --- a/packages/racing/application/use-cases/GetRaceWithSOFUseCase.ts +++ b/packages/racing/application/use-cases/GetRaceWithSOFUseCase.ts @@ -14,13 +14,16 @@ import { AverageStrengthOfFieldCalculator, type StrengthOfFieldCalculator, } from '../../domain/services/StrengthOfFieldCalculator'; -import type { IRaceWithSOFPresenter } from '../presenters/IRaceWithSOFPresenter'; +import type { IRaceWithSOFPresenter, RaceWithSOFResultDTO } from '../presenters/IRaceWithSOFPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; export interface GetRaceWithSOFQueryParams { raceId: string; } -export class GetRaceWithSOFUseCase { +export class GetRaceWithSOFUseCase + implements UseCase +{ private readonly sofCalculator: StrengthOfFieldCalculator; constructor( @@ -28,18 +31,19 @@ export class GetRaceWithSOFUseCase { private readonly registrationRepository: IRaceRegistrationRepository, private readonly resultRepository: IResultRepository, private readonly driverRatingProvider: DriverRatingProvider, - public readonly presenter: IRaceWithSOFPresenter, sofCalculator?: StrengthOfFieldCalculator, ) { this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator(); } - async execute(params: GetRaceWithSOFQueryParams): Promise { + async execute(params: GetRaceWithSOFQueryParams, presenter: IRaceWithSOFPresenter): Promise { + presenter.reset(); + const { raceId } = params; const race = await this.raceRepository.findById(raceId); if (!race) { - return null; + return; } // Get participant IDs based on race status @@ -56,30 +60,34 @@ export class GetRaceWithSOFUseCase { // Use stored SOF if available, otherwise calculate let strengthOfField = race.strengthOfField ?? null; - + if (strengthOfField === null && participantIds.length > 0) { const ratings = this.driverRatingProvider.getRatings(participantIds); const driverRatings = participantIds .filter(id => ratings.has(id)) .map(id => ({ driverId: id, rating: ratings.get(id)! })); - + strengthOfField = this.sofCalculator.calculate(driverRatings); } - this.presenter.present( - race.id, - race.leagueId, - race.scheduledAt, - race.track, - race.trackId, - race.car, - race.carId, - race.sessionType, - race.status, + presenter.reset(); + + const dto: RaceWithSOFResultDTO = { + raceId: race.id, + leagueId: race.leagueId, + scheduledAt: race.scheduledAt, + track: race.track ?? '', + trackId: race.trackId ?? '', + car: race.car ?? '', + carId: race.carId ?? '', + sessionType: race.sessionType, + status: race.status, strengthOfField, - race.registeredCount ?? participantIds.length, - race.maxParticipants, - participantIds.length - ); + registeredCount: race.registeredCount ?? participantIds.length, + maxParticipants: race.maxParticipants ?? participantIds.length, + participantCount: participantIds.length, + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetRacesPageDataUseCase.ts b/packages/racing/application/use-cases/GetRacesPageDataUseCase.ts index 0564fcc7f..eb9d646ba 100644 --- a/packages/racing/application/use-cases/GetRacesPageDataUseCase.ts +++ b/packages/racing/application/use-cases/GetRacesPageDataUseCase.ts @@ -1,15 +1,23 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; -import type { IRacesPagePresenter } from '@gridpilot/racing/application/presenters/IRacesPagePresenter'; +import type { + IRacesPagePresenter, + RacesPageResultDTO, + RacesPageViewModel, +} from '@gridpilot/racing/application/presenters/IRacesPagePresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; -export class GetRacesPageDataUseCase { +export class GetRacesPageDataUseCase + implements UseCase +{ constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, - public readonly presenter: IRacesPagePresenter, ) {} - async execute(): Promise { + async execute(_input: void, presenter: IRacesPagePresenter): Promise { + presenter.reset(); + const [allRaces, allLeagues] = await Promise.all([ this.raceRepository.findAll(), this.leagueRepository.findAll(), @@ -33,6 +41,10 @@ export class GetRacesPageDataUseCase { isPast: race.isPast(), })); - this.presenter.present(races); + const dto: RacesPageResultDTO = { + races, + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetSponsorDashboardUseCase.ts b/packages/racing/application/use-cases/GetSponsorDashboardUseCase.ts index c98c06826..c0d72feb2 100644 --- a/packages/racing/application/use-cases/GetSponsorDashboardUseCase.ts +++ b/packages/racing/application/use-cases/GetSponsorDashboardUseCase.ts @@ -10,7 +10,11 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { ISponsorDashboardPresenter } from '../presenters/ISponsorDashboardPresenter'; +import type { + ISponsorDashboardPresenter, + SponsorDashboardViewModel, +} from '../presenters/ISponsorDashboardPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; export interface GetSponsorDashboardQueryParams { sponsorId: string; @@ -47,7 +51,9 @@ export interface SponsorDashboardDTO { }; } -export class GetSponsorDashboardUseCase { +export class GetSponsorDashboardUseCase + implements UseCase +{ constructor( private readonly sponsorRepository: ISponsorRepository, private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository, @@ -55,15 +61,19 @@ export class GetSponsorDashboardUseCase { private readonly leagueRepository: ILeagueRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly raceRepository: IRaceRepository, - private readonly presenter: ISponsorDashboardPresenter, ) {} - async execute(params: GetSponsorDashboardQueryParams): Promise { + async execute( + params: GetSponsorDashboardQueryParams, + presenter: ISponsorDashboardPresenter, + ): Promise { + presenter.reset(); + const { sponsorId } = params; const sponsor = await this.sponsorRepository.findById(sponsorId); if (!sponsor) { - this.presenter.present(null); + presenter.present(null); return; } @@ -139,11 +149,11 @@ export class GetSponsorDashboardUseCase { // Calculate exposure score (0-100 based on tier distribution) const mainSponsorships = sponsorships.filter(s => s.tier === 'main').length; - const exposure = sponsorships.length > 0 + const exposure = sponsorships.length > 0 ? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10)) : 0; - this.presenter.present({ + const dto: SponsorDashboardDTO = { sponsorId, sponsorName: sponsor.name, metrics: { @@ -162,6 +172,8 @@ export class GetSponsorDashboardUseCase { totalInvestment, costPerThousandViews: Math.round(costPerThousandViews * 100) / 100, }, - }); + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts b/packages/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts index 98b48332f..561327120 100644 --- a/packages/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts +++ b/packages/racing/application/use-cases/GetSponsorSponsorshipsUseCase.ts @@ -11,7 +11,11 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship'; -import type { ISponsorSponsorshipsPresenter } from '../presenters/ISponsorSponsorshipsPresenter'; +import type { + ISponsorSponsorshipsPresenter, + SponsorSponsorshipsViewModel, +} from '../presenters/ISponsorSponsorshipsPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; export interface GetSponsorSponsorshipsQueryParams { sponsorId: string; @@ -62,7 +66,9 @@ export interface SponsorSponsorshipsDTO { }; } -export class GetSponsorSponsorshipsUseCase { +export class GetSponsorSponsorshipsUseCase + implements UseCase +{ constructor( private readonly sponsorRepository: ISponsorRepository, private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository, @@ -70,15 +76,19 @@ export class GetSponsorSponsorshipsUseCase { private readonly leagueRepository: ILeagueRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly raceRepository: IRaceRepository, - private readonly presenter: ISponsorSponsorshipsPresenter, ) {} - async execute(params: GetSponsorSponsorshipsQueryParams): Promise { + async execute( + params: GetSponsorSponsorshipsQueryParams, + presenter: ISponsorSponsorshipsPresenter, + ): Promise { + presenter.reset(); + const { sponsorId } = params; const sponsor = await this.sponsorRepository.findById(sponsorId); if (!sponsor) { - this.presenter.present(null); + presenter.present(null); return; } @@ -150,7 +160,7 @@ export class GetSponsorSponsorshipsUseCase { const activeSponsorships = sponsorships.filter(s => s.status === 'active').length; - this.presenter.present({ + const dto: SponsorSponsorshipsDTO = { sponsorId, sponsorName: sponsor.name, sponsorships: sponsorshipDetails, @@ -161,6 +171,8 @@ export class GetSponsorSponsorshipsUseCase { totalPlatformFees, currency: 'USD', }, - }); + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetTeamDetailsUseCase.ts b/packages/racing/application/use-cases/GetTeamDetailsUseCase.ts index 09a63d9bf..5d3a9e935 100644 --- a/packages/racing/application/use-cases/GetTeamDetailsUseCase.ts +++ b/packages/racing/application/use-cases/GetTeamDetailsUseCase.ts @@ -1,19 +1,31 @@ import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; -import type { ITeamDetailsPresenter } from '../presenters/ITeamDetailsPresenter'; +import type { + ITeamDetailsPresenter, + TeamDetailsResultDTO, + TeamDetailsViewModel, +} from '../presenters/ITeamDetailsPresenter'; +import type { UseCase } from '@gridpilot/shared/application/UseCase'; /** * Use Case for retrieving team details. * Orchestrates domain logic and delegates presentation to the presenter. */ -export class GetTeamDetailsUseCase { +export class GetTeamDetailsUseCase + implements UseCase<{ teamId: string; driverId: string }, TeamDetailsResultDTO, TeamDetailsViewModel, ITeamDetailsPresenter> +{ constructor( private readonly teamRepository: ITeamRepository, private readonly membershipRepository: ITeamMembershipRepository, - public readonly presenter: ITeamDetailsPresenter, ) {} - async execute(teamId: string, driverId: string): Promise { + async execute( + params: { teamId: string; driverId: string }, + presenter: ITeamDetailsPresenter, + ): Promise { + presenter.reset(); + + const { teamId, driverId } = params; const team = await this.teamRepository.findById(teamId); if (!team) { throw new Error('Team not found'); @@ -21,6 +33,12 @@ export class GetTeamDetailsUseCase { const membership = await this.membershipRepository.getMembership(teamId, driverId); - this.presenter.present(team, membership, driverId); + const dto: TeamDetailsResultDTO = { + team, + membership, + driverId, + }; + + presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts b/packages/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts index ec1dd459a..5ef00501d 100644 --- a/packages/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts +++ b/packages/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts @@ -17,6 +17,10 @@ export class PreviewLeagueScheduleUseCase { execute(params: PreviewLeagueScheduleQueryParams): void { const seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule); + if (!seasonSchedule) { + throw new Error('Invalid schedule data'); + } + const maxRounds = params.maxRounds && params.maxRounds > 0 ? Math.min(params.maxRounds, seasonSchedule.plannedRounds) @@ -46,8 +50,11 @@ export class PreviewLeagueScheduleUseCase { return 'No rounds scheduled.'; } - const first = new Date(rounds[0].scheduledAt); - const last = new Date(rounds[rounds.length - 1].scheduledAt); + const firstRound = rounds[0]!; + const lastRound = rounds[rounds.length - 1]!; + + const first = new Date(firstRound.scheduledAt); + const last = new Date(lastRound.scheduledAt); const firstDate = first.toISOString().slice(0, 10); const lastDate = last.toISOString().slice(0, 10); diff --git a/packages/racing/application/use-cases/UpdateTeamUseCase.ts b/packages/racing/application/use-cases/UpdateTeamUseCase.ts index d4d340465..2b3442517 100644 --- a/packages/racing/application/use-cases/UpdateTeamUseCase.ts +++ b/packages/racing/application/use-cases/UpdateTeamUseCase.ts @@ -22,10 +22,12 @@ export class UpdateTeamUseCase { throw new Error('Team not found'); } - const updated: Team = { - ...existing, - ...updates, - }; + const updated = existing.update({ + ...(updates.name !== undefined && { name: updates.name }), + ...(updates.tag !== undefined && { tag: updates.tag }), + ...(updates.description !== undefined && { description: updates.description }), + ...(updates.leagues !== undefined && { leagues: updates.leagues }), + }); await this.teamRepository.update(updated); } diff --git a/packages/racing/domain/entities/Car.ts b/packages/racing/domain/entities/Car.ts index 3598dca0c..2546cb2b0 100644 --- a/packages/racing/domain/entities/Car.ts +++ b/packages/racing/domain/entities/Car.ts @@ -19,9 +19,9 @@ export class Car implements IEntity { readonly carClass: CarClass; readonly license: CarLicense; readonly year: number; - readonly horsepower?: number; - readonly weight?: number; - readonly imageUrl?: string; + readonly horsepower: number | undefined; + readonly weight: number | undefined; + readonly imageUrl: string | undefined; readonly gameId: string; private constructor(props: { diff --git a/packages/racing/domain/entities/Driver.ts b/packages/racing/domain/entities/Driver.ts index 351bff0fc..f7a71b30d 100644 --- a/packages/racing/domain/entities/Driver.ts +++ b/packages/racing/domain/entities/Driver.ts @@ -13,7 +13,7 @@ export class Driver implements IEntity { readonly iracingId: string; readonly name: string; readonly country: string; - readonly bio?: string; + readonly bio: string | undefined; readonly joinedAt: Date; private constructor(props: { @@ -92,14 +92,18 @@ export class Driver implements IEntity { update(props: Partial<{ name: string; country: string; - bio: string; + bio?: string; }>): Driver { + const nextName = props.name ?? this.name; + const nextCountry = props.country ?? this.country; + const nextBio = props.bio ?? this.bio; + return new Driver({ id: this.id, iracingId: this.iracingId, - name: props.name ?? this.name, - country: props.country ?? this.country, - bio: props.bio ?? this.bio, + name: nextName, + country: nextCountry, + ...(nextBio !== undefined ? { bio: nextBio } : {}), joinedAt: this.joinedAt, }); } diff --git a/packages/racing/domain/entities/League.ts b/packages/racing/domain/entities/League.ts index fea67a70a..f450be2de 100644 --- a/packages/racing/domain/entities/League.ts +++ b/packages/racing/domain/entities/League.ts @@ -87,7 +87,7 @@ export class League implements IEntity { readonly ownerId: string; readonly settings: LeagueSettings; readonly createdAt: Date; - readonly socialLinks?: LeagueSocialLinks; + readonly socialLinks: LeagueSocialLinks | undefined; private constructor(props: { id: string; @@ -140,6 +140,8 @@ export class League implements IEntity { stewarding: defaultStewardingSettings, }; + const socialLinks = props.socialLinks; + return new League({ id: props.id, name: props.name, @@ -147,7 +149,7 @@ export class League implements IEntity { ownerId: props.ownerId, settings: { ...defaultSettings, ...props.settings }, createdAt: props.createdAt ?? new Date(), - socialLinks: props.socialLinks, + ...(socialLinks !== undefined ? { socialLinks } : {}), }); } @@ -189,7 +191,7 @@ export class League implements IEntity { description: string; ownerId: string; settings: LeagueSettings; - socialLinks: LeagueSocialLinks | undefined; + socialLinks?: LeagueSocialLinks; }>): League { return new League({ id: this.id, @@ -198,7 +200,11 @@ export class League implements IEntity { ownerId: props.ownerId ?? this.ownerId, settings: props.settings ?? this.settings, createdAt: this.createdAt, - socialLinks: props.socialLinks ?? this.socialLinks, + ...(props.socialLinks !== undefined + ? { socialLinks: props.socialLinks } + : this.socialLinks !== undefined + ? { socialLinks: this.socialLinks } + : {}), }); } } \ No newline at end of file diff --git a/packages/racing/domain/entities/Penalty.ts b/packages/racing/domain/entities/Penalty.ts index c96cdf483..e3c779c21 100644 --- a/packages/racing/domain/entities/Penalty.ts +++ b/packages/racing/domain/entities/Penalty.ts @@ -102,12 +102,16 @@ export class Penalty implements IEntity { if (this.props.status === 'overturned') { throw new RacingDomainInvariantError('Cannot apply an overturned penalty'); } - return new Penalty({ + const base: PenaltyProps = { ...this.props, status: 'applied', appliedAt: new Date(), - notes, - }); + }; + + const next: PenaltyProps = + notes !== undefined ? { ...base, notes } : base; + + return Penalty.create(next); } /** diff --git a/packages/racing/domain/entities/Protest.ts b/packages/racing/domain/entities/Protest.ts index 8f6329bbd..eefcd9255 100644 --- a/packages/racing/domain/entities/Protest.ts +++ b/packages/racing/domain/entities/Protest.ts @@ -153,14 +153,18 @@ export class Protest implements IEntity { if (!statement?.trim()) { throw new RacingDomainValidationError('Defense statement is required'); } + const defenseBase: ProtestDefense = { + statement: statement.trim(), + submittedAt: new Date(), + }; + + const nextDefense: ProtestDefense = + videoUrl !== undefined ? { ...defenseBase, videoUrl } : defenseBase; + return new Protest({ ...this.props, status: 'under_review', - defense: { - statement: statement.trim(), - videoUrl, - submittedAt: new Date(), - }, + defense: nextDefense, }); } diff --git a/packages/racing/domain/entities/Race.ts b/packages/racing/domain/entities/Race.ts index 37f634140..15db4d431 100644 --- a/packages/racing/domain/entities/Race.ts +++ b/packages/racing/domain/entities/Race.ts @@ -16,14 +16,14 @@ export class Race implements IEntity { readonly leagueId: string; readonly scheduledAt: Date; readonly track: string; - readonly trackId?: string; + readonly trackId: string | undefined; readonly car: string; - readonly carId?: string; + readonly carId: string | undefined; readonly sessionType: SessionType; readonly status: RaceStatus; - readonly strengthOfField?: number; - readonly registeredCount?: number; - readonly maxParticipants?: number; + readonly strengthOfField: number | undefined; + readonly registeredCount: number | undefined; + readonly maxParticipants: number | undefined; private constructor(props: { id: string; @@ -127,10 +127,34 @@ export class Race implements IEntity { throw new RacingDomainInvariantError('Only scheduled races can be started'); } - return new Race({ - ...this, - status: 'running', - }); + const base = { + id: this.id, + leagueId: this.leagueId, + scheduledAt: this.scheduledAt, + track: this.track, + car: this.car, + sessionType: this.sessionType, + status: 'running' as RaceStatus, + }; + + const withTrackId = + this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; + const withCarId = + this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; + const withSof = + this.strengthOfField !== undefined + ? { ...withCarId, strengthOfField: this.strengthOfField } + : withCarId; + const withRegistered = + this.registeredCount !== undefined + ? { ...withSof, registeredCount: this.registeredCount } + : withSof; + const props = + this.maxParticipants !== undefined + ? { ...withRegistered, maxParticipants: this.maxParticipants } + : withRegistered; + + return Race.create(props); } /** @@ -145,10 +169,34 @@ export class Race implements IEntity { throw new RacingDomainInvariantError('Cannot complete a cancelled race'); } - return new Race({ - ...this, - status: 'completed', - }); + const base = { + id: this.id, + leagueId: this.leagueId, + scheduledAt: this.scheduledAt, + track: this.track, + car: this.car, + sessionType: this.sessionType, + status: 'completed' as RaceStatus, + }; + + const withTrackId = + this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; + const withCarId = + this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; + const withSof = + this.strengthOfField !== undefined + ? { ...withCarId, strengthOfField: this.strengthOfField } + : withCarId; + const withRegistered = + this.registeredCount !== undefined + ? { ...withSof, registeredCount: this.registeredCount } + : withSof; + const props = + this.maxParticipants !== undefined + ? { ...withRegistered, maxParticipants: this.maxParticipants } + : withRegistered; + + return Race.create(props); } /** @@ -163,21 +211,62 @@ export class Race implements IEntity { throw new RacingDomainInvariantError('Race is already cancelled'); } - return new Race({ - ...this, - status: 'cancelled', - }); + const base = { + id: this.id, + leagueId: this.leagueId, + scheduledAt: this.scheduledAt, + track: this.track, + car: this.car, + sessionType: this.sessionType, + status: 'cancelled' as RaceStatus, + }; + + const withTrackId = + this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; + const withCarId = + this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; + const withSof = + this.strengthOfField !== undefined + ? { ...withCarId, strengthOfField: this.strengthOfField } + : withCarId; + const withRegistered = + this.registeredCount !== undefined + ? { ...withSof, registeredCount: this.registeredCount } + : withSof; + const props = + this.maxParticipants !== undefined + ? { ...withRegistered, maxParticipants: this.maxParticipants } + : withRegistered; + + return Race.create(props); } /** * Update SOF and participant count */ updateField(strengthOfField: number, registeredCount: number): Race { - return new Race({ - ...this, + const base = { + id: this.id, + leagueId: this.leagueId, + scheduledAt: this.scheduledAt, + track: this.track, + car: this.car, + sessionType: this.sessionType, + status: this.status, strengthOfField, registeredCount, - }); + }; + + const withTrackId = + this.trackId !== undefined ? { ...base, trackId: this.trackId } : base; + const withCarId = + this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId; + const props = + this.maxParticipants !== undefined + ? { ...withCarId, maxParticipants: this.maxParticipants } + : withCarId; + + return Race.create(props); } /** diff --git a/packages/racing/domain/entities/Season.ts b/packages/racing/domain/entities/Season.ts index 48532f28a..be326a4e5 100644 --- a/packages/racing/domain/entities/Season.ts +++ b/packages/racing/domain/entities/Season.ts @@ -8,11 +8,11 @@ export class Season implements IEntity { readonly leagueId: string; readonly gameId: string; readonly name: string; - readonly year?: number; - readonly order?: number; + readonly year: number | undefined; + readonly order: number | undefined; readonly status: SeasonStatus; - readonly startDate?: Date; - readonly endDate?: Date; + readonly startDate: Date | undefined; + readonly endDate: Date | undefined; private constructor(props: { id: string; diff --git a/packages/racing/domain/entities/SeasonSponsorship.ts b/packages/racing/domain/entities/SeasonSponsorship.ts index b0658cd3d..798c7d7a1 100644 --- a/packages/racing/domain/entities/SeasonSponsorship.ts +++ b/packages/racing/domain/entities/SeasonSponsorship.ts @@ -33,8 +33,8 @@ export class SeasonSponsorship implements IEntity { readonly pricing: Money; readonly status: SponsorshipStatus; readonly createdAt: Date; - readonly activatedAt?: Date; - readonly description?: string; + readonly activatedAt: Date | undefined; + readonly description: string | undefined; private constructor(props: SeasonSponsorshipProps) { this.id = props.id; @@ -105,11 +105,23 @@ export class SeasonSponsorship implements IEntity { throw new RacingDomainInvariantError('Cannot activate a cancelled SeasonSponsorship'); } - return new SeasonSponsorship({ - ...this, + const base: SeasonSponsorshipProps = { + id: this.id, + seasonId: this.seasonId, + sponsorId: this.sponsorId, + tier: this.tier, + pricing: this.pricing, status: 'active', + createdAt: this.createdAt, activatedAt: new Date(), - }); + }; + + const next: SeasonSponsorshipProps = + this.description !== undefined + ? { ...base, description: this.description } + : base; + + return new SeasonSponsorship(next); } /** @@ -120,10 +132,27 @@ export class SeasonSponsorship implements IEntity { throw new RacingDomainInvariantError('SeasonSponsorship is already cancelled'); } - return new SeasonSponsorship({ - ...this, + const base: SeasonSponsorshipProps = { + id: this.id, + seasonId: this.seasonId, + sponsorId: this.sponsorId, + tier: this.tier, + pricing: this.pricing, status: 'cancelled', - }); + createdAt: this.createdAt, + }; + + const withActivated = + this.activatedAt !== undefined + ? { ...base, activatedAt: this.activatedAt } + : base; + + const next: SeasonSponsorshipProps = + this.description !== undefined + ? { ...withActivated, description: this.description } + : withActivated; + + return new SeasonSponsorship(next); } /** diff --git a/packages/racing/domain/entities/Sponsor.ts b/packages/racing/domain/entities/Sponsor.ts index 2212afa39..941288468 100644 --- a/packages/racing/domain/entities/Sponsor.ts +++ b/packages/racing/domain/entities/Sponsor.ts @@ -20,8 +20,8 @@ export class Sponsor implements IEntity { readonly id: string; readonly name: string; readonly contactEmail: string; - readonly logoUrl?: string; - readonly websiteUrl?: string; + readonly logoUrl: string | undefined; + readonly websiteUrl: string | undefined; readonly createdAt: Date; private constructor(props: SponsorProps) { @@ -35,11 +35,23 @@ export class Sponsor implements IEntity { static create(props: Omit & { createdAt?: Date }): Sponsor { this.validate(props); - - return new Sponsor({ - ...props, - createdAt: props.createdAt ?? new Date(), - }); + + const { createdAt, ...rest } = props; + const base = { + id: rest.id, + name: rest.name, + contactEmail: rest.contactEmail, + createdAt: createdAt ?? new Date(), + }; + + const withLogo = + rest.logoUrl !== undefined ? { ...base, logoUrl: rest.logoUrl } : base; + const withWebsite = + rest.websiteUrl !== undefined + ? { ...withLogo, websiteUrl: rest.websiteUrl } + : withLogo; + + return new Sponsor(withWebsite); } private static validate(props: Omit): void { @@ -80,18 +92,30 @@ export class Sponsor implements IEntity { update(props: Partial<{ name: string; contactEmail: string; - logoUrl: string | undefined; - websiteUrl: string | undefined; + logoUrl?: string; + websiteUrl?: string; }>): Sponsor { - const updated = { + const updatedBase = { id: this.id, name: props.name ?? this.name, contactEmail: props.contactEmail ?? this.contactEmail, - logoUrl: props.logoUrl !== undefined ? props.logoUrl : this.logoUrl, - websiteUrl: props.websiteUrl !== undefined ? props.websiteUrl : this.websiteUrl, createdAt: this.createdAt, }; + const withLogo = + props.logoUrl !== undefined + ? { ...updatedBase, logoUrl: props.logoUrl } + : this.logoUrl !== undefined + ? { ...updatedBase, logoUrl: this.logoUrl } + : updatedBase; + + const updated = + props.websiteUrl !== undefined + ? { ...withLogo, websiteUrl: props.websiteUrl } + : this.websiteUrl !== undefined + ? { ...withLogo, websiteUrl: this.websiteUrl } + : withLogo; + Sponsor.validate(updated); return new Sponsor(updated); } diff --git a/packages/racing/domain/entities/SponsorshipRequest.ts b/packages/racing/domain/entities/SponsorshipRequest.ts index 3f12d7e1c..914577c0c 100644 --- a/packages/racing/domain/entities/SponsorshipRequest.ts +++ b/packages/racing/domain/entities/SponsorshipRequest.ts @@ -36,12 +36,12 @@ export class SponsorshipRequest implements IEntity { readonly entityId: string; readonly tier: SponsorshipTier; readonly offeredAmount: Money; - readonly message?: string; + readonly message: string | undefined; readonly status: SponsorshipRequestStatus; readonly createdAt: Date; - readonly respondedAt?: Date; - readonly respondedBy?: string; - readonly rejectionReason?: string; + readonly respondedAt: Date | undefined; + readonly respondedBy: string | undefined; + readonly rejectionReason: string | undefined; private constructor(props: SponsorshipRequestProps) { this.id = props.id; @@ -113,12 +113,28 @@ export class SponsorshipRequest implements IEntity { throw new RacingDomainValidationError('respondedBy is required when accepting'); } - return new SponsorshipRequest({ - ...this, + const base: SponsorshipRequestProps = { + id: this.id, + sponsorId: this.sponsorId, + entityType: this.entityType, + entityId: this.entityId, + tier: this.tier, + offeredAmount: this.offeredAmount, status: 'accepted', + createdAt: this.createdAt, respondedAt: new Date(), respondedBy, - }); + }; + + const withMessage = + this.message !== undefined ? { ...base, message: this.message } : base; + + const next: SponsorshipRequestProps = + this.rejectionReason !== undefined + ? { ...withMessage, rejectionReason: this.rejectionReason } + : withMessage; + + return new SponsorshipRequest(next); } /** @@ -133,13 +149,26 @@ export class SponsorshipRequest implements IEntity { throw new RacingDomainValidationError('respondedBy is required when rejecting'); } - return new SponsorshipRequest({ - ...this, + const base: SponsorshipRequestProps = { + id: this.id, + sponsorId: this.sponsorId, + entityType: this.entityType, + entityId: this.entityId, + tier: this.tier, + offeredAmount: this.offeredAmount, status: 'rejected', + createdAt: this.createdAt, respondedAt: new Date(), respondedBy, - rejectionReason: reason, - }); + }; + + const withMessage = + this.message !== undefined ? { ...base, message: this.message } : base; + + const next: SponsorshipRequestProps = + reason !== undefined ? { ...withMessage, rejectionReason: reason } : withMessage; + + return new SponsorshipRequest(next); } /** @@ -150,11 +179,34 @@ export class SponsorshipRequest implements IEntity { throw new RacingDomainInvariantError(`Cannot withdraw a ${this.status} sponsorship request`); } - return new SponsorshipRequest({ - ...this, + const base: SponsorshipRequestProps = { + id: this.id, + sponsorId: this.sponsorId, + entityType: this.entityType, + entityId: this.entityId, + tier: this.tier, + offeredAmount: this.offeredAmount, status: 'withdrawn', + createdAt: this.createdAt, respondedAt: new Date(), - }); + }; + + const withRespondedBy = + this.respondedBy !== undefined + ? { ...base, respondedBy: this.respondedBy } + : base; + + const withMessage = + this.message !== undefined + ? { ...withRespondedBy, message: this.message } + : withRespondedBy; + + const next: SponsorshipRequestProps = + this.rejectionReason !== undefined + ? { ...withMessage, rejectionReason: this.rejectionReason } + : withMessage; + + return new SponsorshipRequest(next); } /** diff --git a/packages/racing/domain/entities/Standing.ts b/packages/racing/domain/entities/Standing.ts index 25bcf4415..9ffe48886 100644 --- a/packages/racing/domain/entities/Standing.ts +++ b/packages/racing/domain/entities/Standing.ts @@ -5,7 +5,7 @@ * Immutable entity with factory methods and domain validation. */ -import { RacingDomainValidationError, RacingDomainError } from '../errors/RacingDomainError'; +import { RacingDomainError, RacingDomainValidationError } from '../errors/RacingDomainError'; import type { IEntity } from '@gridpilot/shared/domain'; export class Standing implements IEntity { @@ -104,12 +104,17 @@ export class Standing implements IEntity { */ updatePosition(position: number): Standing { if (!Number.isInteger(position) || position < 1) { - throw new RacingDomainError('Position must be a positive integer'); + throw new RacingDomainValidationError('Position must be a positive integer'); } - return new Standing({ - ...this, + return Standing.create({ + id: this.id, + leagueId: this.leagueId, + driverId: this.driverId, + points: this.points, + wins: this.wins, position, + racesCompleted: this.racesCompleted, }); } diff --git a/packages/racing/domain/entities/Track.ts b/packages/racing/domain/entities/Track.ts index 3fdaaa269..6b9d86331 100644 --- a/packages/racing/domain/entities/Track.ts +++ b/packages/racing/domain/entities/Track.ts @@ -20,7 +20,7 @@ export class Track implements IEntity { readonly difficulty: TrackDifficulty; readonly lengthKm: number; readonly turns: number; - readonly imageUrl?: string; + readonly imageUrl: string | undefined; readonly gameId: string; private constructor(props: { @@ -32,7 +32,7 @@ export class Track implements IEntity { difficulty: TrackDifficulty; lengthKm: number; turns: number; - imageUrl?: string; + imageUrl?: string | undefined; gameId: string; }) { this.id = props.id; @@ -64,7 +64,7 @@ export class Track implements IEntity { }): Track { this.validate(props); - return new Track({ + const base = { id: props.id, name: props.name, shortName: props.shortName ?? props.name.slice(0, 3).toUpperCase(), @@ -73,9 +73,13 @@ export class Track implements IEntity { difficulty: props.difficulty ?? 'intermediate', lengthKm: props.lengthKm, turns: props.turns, - imageUrl: props.imageUrl, gameId: props.gameId, - }); + }; + + const withImage = + props.imageUrl !== undefined ? { ...base, imageUrl: props.imageUrl } : base; + + return new Track(withImage); } /** diff --git a/packages/racing/domain/repositories/ITeamMembershipRepository.ts b/packages/racing/domain/repositories/ITeamMembershipRepository.ts index c1b7b44cc..d07007073 100644 --- a/packages/racing/domain/repositories/ITeamMembershipRepository.ts +++ b/packages/racing/domain/repositories/ITeamMembershipRepository.ts @@ -36,6 +36,11 @@ export interface ITeamMembershipRepository { */ removeMembership(teamId: string, driverId: string): Promise; + /** + * Count active members for a team. + */ + countByTeamId(teamId: string): Promise; + /** * Get all join requests for a team. */ diff --git a/packages/racing/domain/services/EventScoringService.ts b/packages/racing/domain/services/EventScoringService.ts index 39f59de53..e538feecb 100644 --- a/packages/racing/domain/services/EventScoringService.ts +++ b/packages/racing/domain/services/EventScoringService.ts @@ -115,6 +115,10 @@ export class EventScoringService const sortedByLap = [...results].sort((a, b) => a.fastestLap - b.fastestLap); const best = sortedByLap[0]; + if (!best) { + return; + } + const requiresTop = rule.requiresFinishInTopN; if (typeof requiresTop === 'number') { if (best.position <= 0 || best.position > requiresTop) { diff --git a/packages/racing/domain/services/IDriverStatsService.ts b/packages/racing/domain/services/IDriverStatsService.ts new file mode 100644 index 000000000..e3e949c56 --- /dev/null +++ b/packages/racing/domain/services/IDriverStatsService.ts @@ -0,0 +1,13 @@ +import type { IDomainService } from '@gridpilot/shared/domain'; + +export interface DriverStats { + rating: number; + wins: number; + podiums: number; + totalRaces: number; + overallRank: number | null; +} + +export interface IDriverStatsService extends IDomainService { + getDriverStats(driverId: string): DriverStats | null; +} \ No newline at end of file diff --git a/packages/racing/domain/services/IRankingService.ts b/packages/racing/domain/services/IRankingService.ts new file mode 100644 index 000000000..c7137911e --- /dev/null +++ b/packages/racing/domain/services/IRankingService.ts @@ -0,0 +1,11 @@ +import type { IDomainService } from '@gridpilot/shared/domain'; + +export interface DriverRanking { + driverId: string; + rating: number; + overallRank: number | null; +} + +export interface IRankingService extends IDomainService { + getAllDriverRankings(): DriverRanking[]; +} \ No newline at end of file diff --git a/packages/racing/domain/services/SeasonScheduleGenerator.ts b/packages/racing/domain/services/SeasonScheduleGenerator.ts index 94d384392..153edf029 100644 --- a/packages/racing/domain/services/SeasonScheduleGenerator.ts +++ b/packages/racing/domain/services/SeasonScheduleGenerator.ts @@ -1,7 +1,7 @@ import { SeasonSchedule } from '../value-objects/SeasonSchedule'; import { ScheduledRaceSlot } from '../value-objects/ScheduledRaceSlot'; import type { RecurrenceStrategy } from '../value-objects/RecurrenceStrategy'; -import { RacingDomainValidationError, RacingDomainError } from '../errors/RacingDomainError'; +import { RacingDomainError, RacingDomainValidationError } from '../errors/RacingDomainError'; import { RaceTimeOfDay } from '../value-objects/RaceTimeOfDay'; import type { Weekday } from '../types/Weekday'; import { weekdayToIndex } from '../types/Weekday'; @@ -163,7 +163,7 @@ export class SeasonScheduleGenerator { static generateSlotsUpTo(schedule: SeasonSchedule, maxRounds: number): ScheduledRaceSlot[] { if (!Number.isInteger(maxRounds) || maxRounds <= 0) { - throw new RacingDomainError('maxRounds must be a positive integer'); + throw new RacingDomainValidationError('maxRounds must be a positive integer'); } const recurrence: RecurrenceStrategy = schedule.recurrence; diff --git a/packages/racing/domain/value-objects/GameConstraints.ts b/packages/racing/domain/value-objects/GameConstraints.ts index 951f82797..16412560c 100644 --- a/packages/racing/domain/value-objects/GameConstraints.ts +++ b/packages/racing/domain/value-objects/GameConstraints.ts @@ -24,7 +24,7 @@ export interface GameConstraintsProps { /** * Game-specific constraints for popular sim racing games */ -const GAME_CONSTRAINTS: Record = { +const GAME_CONSTRAINTS: Record & { default: GameConstraintsData } = { iracing: { maxDrivers: 64, maxTeams: 32, @@ -76,6 +76,15 @@ const GAME_CONSTRAINTS: Record = { }, }; +function getConstraintsForId(gameId: string): GameConstraintsData { + const lower = gameId.toLowerCase(); + const fromMap = GAME_CONSTRAINTS[lower]; + if (fromMap) { + return fromMap; + } + return GAME_CONSTRAINTS.default; +} + export class GameConstraints implements IValueObject { readonly gameId: string; readonly constraints: GameConstraintsData; @@ -100,8 +109,8 @@ export class GameConstraints implements IValueObject { * Get constraints for a specific game */ static forGame(gameId: string): GameConstraints { + const constraints = getConstraintsForId(gameId); const lowerId = gameId.toLowerCase(); - const constraints = GAME_CONSTRAINTS[lowerId] ?? GAME_CONSTRAINTS.default; return new GameConstraints(lowerId, constraints); } diff --git a/packages/racing/domain/value-objects/SponsorshipPricing.ts b/packages/racing/domain/value-objects/SponsorshipPricing.ts index ccb007fa6..ed733a162 100644 --- a/packages/racing/domain/value-objects/SponsorshipPricing.ts +++ b/packages/racing/domain/value-objects/SponsorshipPricing.ts @@ -17,17 +17,17 @@ export interface SponsorshipSlotConfig { } export interface SponsorshipPricingProps { - mainSlot?: SponsorshipSlotConfig; - secondarySlots?: SponsorshipSlotConfig; + mainSlot?: SponsorshipSlotConfig | undefined; + secondarySlots?: SponsorshipSlotConfig | undefined; acceptingApplications: boolean; - customRequirements?: string; + customRequirements?: string | undefined; } export class SponsorshipPricing implements IValueObject { - readonly mainSlot?: SponsorshipSlotConfig; - readonly secondarySlots?: SponsorshipSlotConfig; + readonly mainSlot: SponsorshipSlotConfig | undefined; + readonly secondarySlots: SponsorshipSlotConfig | undefined; readonly acceptingApplications: boolean; - readonly customRequirements?: string; + readonly customRequirements: string | undefined; private constructor(props: SponsorshipPricingProps) { this.mainSlot = props.mainSlot; @@ -212,8 +212,10 @@ export class SponsorshipPricing implements IValueObject maxSlots: 1, }; + const base = this.props; + return new SponsorshipPricing({ - ...this, + ...base, mainSlot: { ...currentMain, ...config, @@ -234,8 +236,10 @@ export class SponsorshipPricing implements IValueObject maxSlots: 2, }; + const base = this.props; + return new SponsorshipPricing({ - ...this, + ...base, secondarySlots: { ...currentSecondary, ...config, @@ -248,8 +252,10 @@ export class SponsorshipPricing implements IValueObject * Enable/disable accepting applications */ setAcceptingApplications(accepting: boolean): SponsorshipPricing { + const base = this.props; + return new SponsorshipPricing({ - ...this, + ...base, acceptingApplications: accepting, }); } diff --git a/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts b/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts index d8f8377ca..fa6a73b84 100644 --- a/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts @@ -12,11 +12,11 @@ import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRac export class InMemoryResultRepository implements IResultRepository { private results: Map; - private raceRepository?: IRaceRepository; + private raceRepository: IRaceRepository | null; - constructor(seedData?: Result[], raceRepository?: IRaceRepository) { + constructor(seedData?: Result[], raceRepository?: IRaceRepository | null) { this.results = new Map(); - this.raceRepository = raceRepository; + this.raceRepository = raceRepository ?? null; if (seedData) { seedData.forEach(result => { diff --git a/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts b/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts index ee2fcaed4..fc6bf83bb 100644 --- a/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts @@ -28,20 +28,20 @@ const POINTS_SYSTEMS: Record> = { export class InMemoryStandingRepository implements IStandingRepository { private standings: Map; - private resultRepository?: IResultRepository; - private raceRepository?: IRaceRepository; - private leagueRepository?: ILeagueRepository; + private resultRepository: IResultRepository | null; + private raceRepository: IRaceRepository | null; + private leagueRepository: ILeagueRepository | null; constructor( seedData?: Standing[], - resultRepository?: IResultRepository, - raceRepository?: IRaceRepository, - leagueRepository?: ILeagueRepository + resultRepository?: IResultRepository | null, + raceRepository?: IRaceRepository | null, + leagueRepository?: ILeagueRepository | null ) { this.standings = new Map(); - this.resultRepository = resultRepository; - this.raceRepository = raceRepository; - this.leagueRepository = leagueRepository; + this.resultRepository = resultRepository ?? null; + this.raceRepository = raceRepository ?? null; + this.leagueRepository = leagueRepository ?? null; if (seedData) { seedData.forEach(standing => { @@ -123,9 +123,16 @@ export class InMemoryStandingRepository implements IStandingRepository { } // Get points system - const pointsSystem = league.settings.customPoints ?? - POINTS_SYSTEMS[league.settings.pointsSystem] ?? - POINTS_SYSTEMS['f1-2024']; + const resolvedPointsSystem = + league.settings.customPoints ?? + POINTS_SYSTEMS[league.settings.pointsSystem] ?? + POINTS_SYSTEMS['f1-2024']; + + if (!resolvedPointsSystem) { + throw new Error('No points system configured for league'); + } + + const pointsSystem: Record = resolvedPointsSystem; // Get all completed races for the league const races = await this.raceRepository.findCompletedByLeagueId(leagueId); diff --git a/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts b/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts index ae6c2434d..0e7e52803 100644 --- a/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts @@ -76,6 +76,11 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito return [...(this.membershipsByTeam.get(teamId) ?? [])]; } + async countByTeamId(teamId: string): Promise { + const list = this.membershipsByTeam.get(teamId) ?? []; + return list.filter((m) => m.status === 'active').length; + } + async saveMembership(membership: TeamMembership): Promise { const list = this.getMembershipList(membership.teamId); const existingIndex = list.findIndex( diff --git a/packages/racing/tsconfig.json b/packages/racing/tsconfig.json index 5558a6a0b..86e44fbd0 100644 --- a/packages/racing/tsconfig.json +++ b/packages/racing/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": ".", "outDir": "dist", diff --git a/packages/shared/domain/Option.ts b/packages/shared/domain/Option.ts new file mode 100644 index 000000000..e11008d1b --- /dev/null +++ b/packages/shared/domain/Option.ts @@ -0,0 +1,7 @@ +export function coalesce(value: T | undefined | null, fallback: T): T { + return value ?? fallback; +} + +export function present(value: T | undefined | null): T | undefined { + return value === undefined || value === null ? undefined : value; +} \ No newline at end of file diff --git a/packages/shared/domain/index.ts b/packages/shared/domain/index.ts index 5965dea3c..5ef5f3fc4 100644 --- a/packages/shared/domain/index.ts +++ b/packages/shared/domain/index.ts @@ -1,3 +1,4 @@ export * from './Entity'; export * from './ValueObject'; -export * from './Service'; \ No newline at end of file +export * from './Service'; +export * from './Option'; \ No newline at end of file diff --git a/packages/social/tsconfig.json b/packages/social/tsconfig.json index 60fdbff9b..0a6cf0650 100644 --- a/packages/social/tsconfig.json +++ b/packages/social/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": ".", "outDir": "dist", diff --git a/packages/testing-support/src/images/images.ts b/packages/testing-support/src/images/images.ts index aa16f43df..1e7095e0a 100644 --- a/packages/testing-support/src/images/images.ts +++ b/packages/testing-support/src/images/images.ts @@ -33,17 +33,20 @@ function hashString(input: string): number { export function getDriverAvatar(driverId: string): string { const index = hashString(driverId) % DRIVER_AVATARS.length; - return DRIVER_AVATARS[index]; + const avatar = DRIVER_AVATARS[index] ?? DRIVER_AVATARS[0]; + return avatar; } export function getTeamLogo(teamId: string): string { const index = hashString(teamId) % TEAM_LOGOS.length; - return TEAM_LOGOS[index]; + const logo = TEAM_LOGOS[index] ?? TEAM_LOGOS[0]; + return logo; } export function getLeagueBanner(leagueId: string): string { const index = hashString(leagueId) % LEAGUE_BANNERS.length; - return LEAGUE_BANNERS[index]; + const banner = LEAGUE_BANNERS[index] ?? LEAGUE_BANNERS[0]; + return banner; } export interface LeagueCoverImage { diff --git a/packages/testing-support/src/media/DemoAvatarGenerationAdapter.ts b/packages/testing-support/src/media/DemoAvatarGenerationAdapter.ts index ca6582c04..748ba1ff6 100644 --- a/packages/testing-support/src/media/DemoAvatarGenerationAdapter.ts +++ b/packages/testing-support/src/media/DemoAvatarGenerationAdapter.ts @@ -81,7 +81,7 @@ export class DemoAvatarGenerationAdapter implements AvatarGenerationPort { // For demo, return placeholder URLs based on suit color // In production, these would be actual AI-generated images - const colorAvatars = this.placeholderAvatars[options.suitColor] ?? this.placeholderAvatars.blue; + const colorAvatars = this.getPlaceholderAvatars(options.suitColor) ?? []; // Generate unique URLs with a hash to simulate different generations const hash = this.generateHash((options.facePhotoUrl ?? '') + Date.now()); @@ -104,6 +104,14 @@ export class DemoAvatarGenerationAdapter implements AvatarGenerationPort { return new Promise((resolve) => setTimeout(resolve, ms)); } + private getPlaceholderAvatars(color: string): string[] | undefined { + const avatars = this.placeholderAvatars[color]; + if (!avatars || avatars.length === 0) { + return this.placeholderAvatars.blue; + } + return avatars; + } + private generateHash(input: string): string { let hash = 0; for (let i = 0; i < input.length; i += 1) { diff --git a/packages/testing-support/src/media/DemoImageServiceAdapter.ts b/packages/testing-support/src/media/DemoImageServiceAdapter.ts index 0b378039d..83ff25f7a 100644 --- a/packages/testing-support/src/media/DemoImageServiceAdapter.ts +++ b/packages/testing-support/src/media/DemoImageServiceAdapter.ts @@ -7,7 +7,8 @@ export class DemoImageServiceAdapter implements ImageServicePort { getDriverAvatar(driverId: string): string { const numericSuffixMatch = driverId.match(/(\d+)$/); if (numericSuffixMatch) { - const numericSuffix = Number.parseInt(numericSuffixMatch[1], 10); + const numericSuffixString = numericSuffixMatch[1] ?? ''; + const numericSuffix = Number.parseInt(numericSuffixString, 10); return numericSuffix % 2 === 0 ? FEMALE_DEFAULT_AVATAR : MALE_DEFAULT_AVATAR; } diff --git a/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts b/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts index 0252ee8fb..8b9b2bb43 100644 --- a/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts +++ b/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts @@ -38,7 +38,14 @@ export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepo async findLatestByUserId(userId: string): Promise { const userRequests = await this.findByUserId(userId); - return userRequests.length > 0 ? userRequests[0] : null; + if (userRequests.length === 0) { + return null; + } + const latest = userRequests[0]; + if (!latest) { + return null; + } + return latest; } async delete(id: string): Promise { diff --git a/packages/testing-support/src/racing/RacingFeedSeed.ts b/packages/testing-support/src/racing/RacingFeedSeed.ts index 98d97b5a0..84f871f8b 100644 --- a/packages/testing-support/src/racing/RacingFeedSeed.ts +++ b/packages/testing-support/src/racing/RacingFeedSeed.ts @@ -23,11 +23,12 @@ export function createFeedEvents( const completedRaces = races.filter((race) => race.status === 'completed'); // Focus the global feed around a stable “core” of demo drivers - const coreDrivers = faker.helpers.shuffle(drivers).slice(0, 16); + const coreDrivers = faker.helpers.shuffle(drivers).slice(0, Math.min(16, drivers.length)); coreDrivers.forEach((driver, index) => { const league = pickOne(leagues); - const race = completedRaces[index % Math.max(1, completedRaces.length)]; + const raceSource = completedRaces.length > 0 ? completedRaces : races; + const race = pickOne(raceSource); const minutesAgo = 10 + index * 5; const baseTimestamp = new Date(now.getTime() - minutesAgo * 60 * 1000); @@ -166,23 +167,54 @@ export function buildFriends( drivers: Driver[], memberships: RacingMembership[], ): FriendDTO[] { - return drivers.map((driver) => ({ - driverId: driver.id, - displayName: driver.name, - avatarUrl: getDriverAvatar(driver.id), - isOnline: true, - lastSeen: new Date(), - primaryLeagueId: memberships.find((m) => m.driverId === driver.id)?.leagueId, - primaryTeamId: memberships.find((m) => m.driverId === driver.id)?.teamId, - })); + return drivers.map((driver) => { + const membership = memberships.find((m) => m.driverId === driver.id); + + const base: FriendDTO = { + driverId: driver.id, + displayName: driver.name, + avatarUrl: getDriverAvatar(driver.id), + isOnline: true, + lastSeen: new Date(), + }; + + const withLeague = + membership?.leagueId !== undefined + ? { ...base, primaryLeagueId: membership.leagueId } + : base; + + const withTeam = + membership?.teamId !== undefined + ? { ...withLeague, primaryTeamId: membership.teamId } + : withLeague; + + return withTeam; + }); } /** * Build top leagues with banner URLs for UI. */ -export function buildTopLeagues(leagues: League[]): Array { +export type LeagueWithBannerDTO = { + id: string; + name: string; + description: string; + ownerId: string; + settings: League['settings']; + createdAt: Date; + socialLinks: League['socialLinks']; + bannerUrl: string; +}; + +export function buildTopLeagues(leagues: League[]): LeagueWithBannerDTO[] { return leagues.map((league) => ({ - ...league, + id: league.id, + name: league.name, + description: league.description, + ownerId: league.ownerId, + settings: league.settings, + createdAt: league.createdAt, + socialLinks: league.socialLinks, bannerUrl: getLeagueBanner(league.id), })); } @@ -252,5 +284,9 @@ export function buildLatestResults( * Kept here to avoid importing from core in callers that only care about feed. */ function pickOne(items: readonly T[]): T { - return items[Math.floor(faker.number.int({ min: 0, max: items.length - 1 }))]; + if (items.length === 0) { + throw new Error('pickOne: empty items array'); + } + const index = faker.number.int({ min: 0, max: items.length - 1 }); + return items[index]!; } \ No newline at end of file diff --git a/packages/testing-support/src/racing/RacingSeedCore.ts b/packages/testing-support/src/racing/RacingSeedCore.ts index f4ee2b9c3..8f1548f54 100644 --- a/packages/testing-support/src/racing/RacingSeedCore.ts +++ b/packages/testing-support/src/racing/RacingSeedCore.ts @@ -47,7 +47,11 @@ export const POINTS_TABLE: Record = { }; export function pickOne(items: readonly T[]): T { - return items[Math.floor(faker.number.int({ min: 0, max: items.length - 1 }))]; + if (items.length === 0) { + throw new Error('pickOne: empty items array'); + } + const index = faker.number.int({ min: 0, max: items.length - 1 }); + return items[index]!; } export function createDrivers(count: number): Driver[] { @@ -136,18 +140,31 @@ export function createLeagues(ownerIds: string[]): League[] { websiteUrl: 'https://virtual-touring.example.com', } : undefined; - - leagues.push( - League.create({ - id, - name, - description: faker.lorem.sentence(), - ownerId, - settings, - createdAt: faker.date.past(), - socialLinks, - }), - ); + + if (socialLinks) { + leagues.push( + League.create({ + id, + name, + description: faker.lorem.sentence(), + ownerId, + settings, + createdAt: faker.date.past(), + socialLinks, + }), + ); + } else { + leagues.push( + League.create({ + id, + name, + description: faker.lorem.sentence(), + ownerId, + settings, + createdAt: faker.date.past(), + }), + ); + } } return leagues; @@ -204,11 +221,16 @@ export function createMemberships( ? pickOne(leagueTeams) : undefined; - memberships.push({ + const membership: RacingMembership = { driverId: driver.id, leagueId: league.id, - teamId: team?.id, - }); + }; + + if (team) { + membership.teamId = team.id; + } + + memberships.push(membership); }); }); @@ -354,6 +376,7 @@ export function createFriendships(drivers: Driver[]): Friendship[] { for (let offset = 1; offset <= friendCount; offset++) { const friendIndex = (index + offset) % drivers.length; const friend = drivers[friendIndex]; + if (!friend) continue; if (friend.id === driver.id) continue; friendships.push({ diff --git a/packages/testing-support/src/racing/RacingSponsorshipSeed.ts b/packages/testing-support/src/racing/RacingSponsorshipSeed.ts index bb30a5ac8..2cddc9ead 100644 --- a/packages/testing-support/src/racing/RacingSponsorshipSeed.ts +++ b/packages/testing-support/src/racing/RacingSponsorshipSeed.ts @@ -334,12 +334,14 @@ export function createSponsorshipRequests( // Pending request: Simucube wants to sponsor a driver if (drivers.length > 6) { - requests.push( - SponsorshipRequest.create({ - id: 'req-simucube-driver-1', - sponsorId: SIMUCUBE_ID, - entityType: 'driver', - entityId: drivers[5].id, + const targetDriver = drivers[5]; + if (targetDriver) { + requests.push( + SponsorshipRequest.create({ + id: 'req-simucube-driver-1', + sponsorId: SIMUCUBE_ID, + entityType: 'driver', + entityId: targetDriver.id, tier: 'main', offeredAmount: Money.create(250, 'USD'), message: @@ -347,23 +349,27 @@ export function createSponsorshipRequests( createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000), // 2 days ago }), ); + } } // Pending request: Heusinkveld wants to sponsor a team if (teams.length > 3) { - requests.push( - SponsorshipRequest.create({ - id: 'req-heusinkveld-team-1', - sponsorId: HEUSINKVELD_ID, - entityType: 'team', - entityId: teams[2].id, - tier: 'main', - offeredAmount: Money.create(550, 'USD'), - message: - 'Heusinkveld pedals are known for their precision. We believe your team embodies the same values.', - createdAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago - }), - ); + const targetTeam = teams[2]; + if (targetTeam) { + requests.push( + SponsorshipRequest.create({ + id: 'req-heusinkveld-team-1', + sponsorId: HEUSINKVELD_ID, + entityType: 'team', + entityId: targetTeam.id, + tier: 'main', + offeredAmount: Money.create(550, 'USD'), + message: + 'Heusinkveld pedals are known for their precision. We believe your team embodies the same values.', + createdAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago + }), + ); + } } // Pending request: Trak Racer wants to sponsor a race @@ -403,12 +409,14 @@ export function createSponsorshipRequests( // Already accepted request (for history) if (teams.length > 0) { - requests.push( - SponsorshipRequest.create({ - id: 'req-simlab-team-accepted', - sponsorId: SIMLAB_ID, - entityType: 'team', - entityId: teams[0].id, + const acceptedTeam = teams[0]; + if (acceptedTeam) { + requests.push( + SponsorshipRequest.create({ + id: 'req-simlab-team-accepted', + sponsorId: SIMLAB_ID, + entityType: 'team', + entityId: acceptedTeam.id, tier: 'secondary', offeredAmount: Money.create(300, 'USD'), message: 'Sim-Lab rigs are the foundation of any competitive setup.', @@ -416,16 +424,19 @@ export function createSponsorshipRequests( createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), // 30 days ago }), ); + } } // Already rejected request (for history) if (drivers.length > 10) { - requests.push( - SponsorshipRequest.create({ - id: 'req-motionrig-driver-rejected', - sponsorId: MOTIONRIG_ID, - entityType: 'driver', - entityId: drivers[10].id, + const rejectedDriver = drivers[10]; + if (rejectedDriver) { + requests.push( + SponsorshipRequest.create({ + id: 'req-motionrig-driver-rejected', + sponsorId: MOTIONRIG_ID, + entityType: 'driver', + entityId: rejectedDriver.id, tier: 'main', offeredAmount: Money.create(150, 'USD'), message: 'Would you like to represent MotionRig Pro?', @@ -433,6 +444,7 @@ export function createSponsorshipRequests( createdAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000), // 20 days ago }), ); + } } return requests; diff --git a/packages/testing-support/tsconfig.json b/packages/testing-support/tsconfig.json index 5f651fd4a..0cd835ce4 100644 --- a/packages/testing-support/tsconfig.json +++ b/packages/testing-support/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist", diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..2ad88117c --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,56 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM"], + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "alwaysStrict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": false, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + "@/lib/*": ["apps/website/lib/*"], + "@/components/*": ["apps/website/components/*"], + "@/app/*": ["apps/website/app/*"], + "packages/*": ["packages/*"], + "apps/*": ["apps/*"], + "@gridpilot/shared-result": ["packages/shared/result/Result.ts"], + "@gridpilot/shared": ["packages/shared/index.ts"], + "@gridpilot/shared/application": ["packages/shared/application"], + "@gridpilot/shared/application/*": ["packages/shared/application/*"], + "@gridpilot/shared/presentation": ["packages/shared/presentation"], + "@gridpilot/shared/presentation/*": ["packages/shared/presentation/*"], + "@gridpilot/shared/domain": ["packages/shared/domain"], + "@gridpilot/shared/domain/*": ["packages/shared/domain/*"], + "@gridpilot/shared/errors": ["packages/shared/errors"], + "@gridpilot/shared/errors/*": ["packages/shared/errors/*"], + "@gridpilot/automation": ["packages/automation/index.ts"], + "@gridpilot/automation/*": ["packages/automation/*"], + "@gridpilot/identity": ["packages/identity/index.ts"], + "@gridpilot/identity/*": ["packages/identity/*"], + "@gridpilot/media": ["packages/media/index.ts"], + "@gridpilot/media/*": ["packages/media/*"], + "@gridpilot/racing": ["packages/racing/index.ts"], + "@gridpilot/racing/*": ["packages/racing/*"], + "@gridpilot/social": ["packages/social/index.ts"], + "@gridpilot/social/*": ["packages/social/*"], + "@gridpilot/testing-support": ["packages/testing-support/index.ts"], + "@gridpilot/testing-support/*": ["packages/testing-support/*"], + "@gridpilot/analytics": ["packages/analytics/index.ts"], + "@gridpilot/analytics/*": ["packages/analytics/*"], + "@gridpilot/notifications": ["packages/notifications/application/index.ts"], + "@gridpilot/notifications/*": ["packages/notifications/*"] + } + } +} \ No newline at end of file diff --git a/tsconfig.electron.json b/tsconfig.electron.json index d6bb5b9e5..7d6f6603b 100644 --- a/tsconfig.electron.json +++ b/tsconfig.electron.json @@ -1,13 +1,11 @@ { - "extends": "./tsconfig.json", + "extends": "./tsconfig.base.json", "compilerOptions": { "module": "commonjs", "target": "ES2020", "outDir": "dist/main", "jsx": "react-jsx", "lib": ["ES2020", "DOM"], - "skipLibCheck": true, - "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "noEmit": false diff --git a/tsconfig.json b/tsconfig.json index fb248c97e..0779dd9bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,46 +1,5 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "lib": ["ES2022", "DOM"], - "moduleResolution": "node", - "esModuleInterop": true, - "strict": true, - "noImplicitAny": true, - "noImplicitThis": true, - "alwaysStrict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "baseUrl": ".", - "paths": { - "@/*": ["./*"], - "@/lib/*": ["apps/website/lib/*"], - "@/components/*": ["apps/website/components/*"], - "@/app/*": ["apps/website/app/*"], - "packages/*": ["packages/*"], - "apps/*": ["apps/*"], - "@gridpilot/shared-result": ["packages/shared/result/Result.ts"], - "@gridpilot/shared": ["packages/shared/index.ts"], - "@gridpilot/shared/application": ["packages/shared/application"], - "@gridpilot/shared/application/*": ["packages/shared/application/*"], - "@gridpilot/shared/presentation": ["packages/shared/presentation"], - "@gridpilot/shared/presentation/*": ["packages/shared/presentation/*"], - "@gridpilot/shared/domain": ["packages/shared/domain"], - "@gridpilot/shared/domain/*": ["packages/shared/domain/*"], - "@gridpilot/shared/errors": ["packages/shared/errors"], - "@gridpilot/shared/errors/*": ["packages/shared/errors/*"], - "@gridpilot/automation/*": ["packages/automation/*"], - "@gridpilot/testing-support": ["packages/testing-support/index.ts"], - "@gridpilot/media": ["packages/media/index.ts"] - }, - "types": ["vitest/globals", "node"], - "jsx": "react-jsx" - }, + "extends": "./tsconfig.base.json", "include": [ "packages/**/*", "apps/**/*", diff --git a/tsconfig.tests.json b/tsconfig.tests.json index 64f4f836b..c2eb58185 100644 --- a/tsconfig.tests.json +++ b/tsconfig.tests.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.json", + "extends": "./tsconfig.base.json", "include": [ "tests/**/*.ts", "tests/**/*.tsx"