This commit is contained in:
2025-12-12 01:11:36 +01:00
parent ec3ddc3a5c
commit 6a88fe93ab
125 changed files with 1513 additions and 803 deletions

View File

@@ -255,7 +255,18 @@ export function configureDIContainer(): void {
} }
// Overlay Sync Service - create singleton instance directly // 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 = { const publisher = {
publish: async (event: unknown) => { publish: async (event: unknown) => {
try { try {

View File

@@ -374,9 +374,10 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
// Subscribe to automation adapter lifecycle events and relay to renderer // Subscribe to automation adapter lifecycle events and relay to renderer
try { try {
if (!lifecycleSubscribed) { if (!lifecycleSubscribed) {
const lifecycleEmitter = container.getBrowserAutomation() as unknown as IAutomationLifecycleEmitter; const browserAutomation = container.getBrowserAutomation();
if (typeof lifecycleEmitter.onLifecycle === 'function') { const candidate = browserAutomation as Partial<IAutomationLifecycleEmitter>;
lifecycleEmitter.onLifecycle((ev) => { if (typeof candidate.onLifecycle === 'function' && typeof candidate.offLifecycle === 'function') {
candidate.onLifecycle((ev) => {
try { try {
if (mainWindow && mainWindow.webContents) { if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.send('automation-event', ev); mainWindow.webContents.send('automation-event', ev);
@@ -388,6 +389,8 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
}); });
lifecycleSubscribed = true; lifecycleSubscribed = true;
logger.debug('Subscribed to adapter lifecycle events for renderer relay'); logger.debug('Subscribed to adapter lifecycle events for renderer relay');
} else {
logger.debug?.('Browser automation does not expose lifecycle events; skipping subscription');
} }
} }
} catch (e) { } catch (e) {

View File

@@ -31,12 +31,16 @@ export interface CheckoutConfirmationRequest {
timeoutMs: number; timeoutMs: number;
} }
export interface StartAutomationResponse {
success: boolean;
sessionId?: string;
error?: string;
authRequired?: boolean;
authState?: AuthenticationState;
}
export interface ElectronAPI { export interface ElectronAPI {
startAutomation: (config: HostedSessionConfig) => Promise<{ startAutomation: (config: HostedSessionConfig) => Promise<StartAutomationResponse>;
success: boolean;
sessionId?: string;
error?: string;
}>;
stopAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>; stopAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
getSessionStatus: (sessionId: string) => Promise<any>; getSessionStatus: (sessionId: string) => Promise<any>;
pauseAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>; pauseAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
@@ -60,7 +64,8 @@ export interface ElectronAPI {
} }
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
startAutomation: (config: HostedSessionConfig) => ipcRenderer.invoke('start-automation', config), startAutomation: (config: HostedSessionConfig) =>
ipcRenderer.invoke('start-automation', config) as Promise<StartAutomationResponse>,
stopAutomation: (sessionId: string) => ipcRenderer.invoke('stop-automation', sessionId), stopAutomation: (sessionId: string) => ipcRenderer.invoke('stop-automation', sessionId),
getSessionStatus: (sessionId: string) => ipcRenderer.invoke('get-session-status', sessionId), getSessionStatus: (sessionId: string) => ipcRenderer.invoke('get-session-status', sessionId),
pauseAutomation: (sessionId: string) => ipcRenderer.invoke('pause-automation', sessionId), pauseAutomation: (sessionId: string) => ipcRenderer.invoke('pause-automation', sessionId),

View File

@@ -6,6 +6,8 @@ import { BrowserModeToggle } from './components/BrowserModeToggle';
import { CheckoutConfirmationDialog } from './components/CheckoutConfirmationDialog'; import { CheckoutConfirmationDialog } from './components/CheckoutConfirmationDialog';
import { RaceCreationSuccessScreen } from './components/RaceCreationSuccessScreen'; import { RaceCreationSuccessScreen } from './components/RaceCreationSuccessScreen';
import type { HostedSessionConfig } from '../../../packages/automation/domain/types/HostedSessionConfig'; 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 { interface SessionProgress {
sessionId: string; sessionId: string;
@@ -16,7 +18,7 @@ interface SessionProgress {
errorMessage: string | null; errorMessage: string | null;
} }
type AuthState = 'UNKNOWN' | 'AUTHENTICATED' | 'EXPIRED' | 'LOGGED_OUT' | 'CHECKING'; type AuthState = AuthenticationState | 'CHECKING';
type LoginStatus = 'idle' | 'waiting' | 'success' | 'error'; type LoginStatus = 'idle' | 'waiting' | 'success' | 'error';
export function App() { export function App() {
@@ -84,7 +86,7 @@ export function App() {
try { try {
const result = await window.electronAPI.checkAuth(); const result = await window.electronAPI.checkAuth();
if (result.success && result.state) { if (result.success && result.state) {
setAuthState(result.state as AuthState); setAuthState(result.state);
} else { } else {
setAuthError(result.error); setAuthError(result.error);
setAuthState('UNKNOWN'); setAuthState('UNKNOWN');
@@ -103,7 +105,7 @@ export function App() {
try { try {
const result = await window.electronAPI.checkAuth(); const result = await window.electronAPI.checkAuth();
if (result.success && result.state) { if (result.success && result.state) {
setAuthState(result.state as AuthState); setAuthState(result.state);
} else { } else {
setAuthError(result.error || 'Failed to check authentication'); setAuthError(result.error || 'Failed to check authentication');
setAuthState('UNKNOWN'); setAuthState('UNKNOWN');
@@ -138,13 +140,7 @@ export function App() {
const handleStartAutomation = async (config: HostedSessionConfig) => { const handleStartAutomation = async (config: HostedSessionConfig) => {
setIsRunning(true); setIsRunning(true);
const result = await window.electronAPI.startAutomation(config) as { const result: StartAutomationResponse = await window.electronAPI.startAutomation(config);
success: boolean;
sessionId?: string;
error?: string;
authRequired?: boolean;
authState?: AuthState;
};
if (result.success && result.sessionId) { if (result.success && result.sessionId) {
setSessionId(result.sessionId); setSessionId(result.sessionId);
@@ -153,8 +149,8 @@ export function App() {
setIsRunning(false); setIsRunning(false);
if ('authRequired' in result && result.authRequired) { if (result.authRequired) {
const nextAuthState = result.authState as AuthState | undefined; const nextAuthState = result.authState;
setAuthState(nextAuthState ?? 'EXPIRED'); setAuthState(nextAuthState ?? 'EXPIRED');
setAuthError(result.error ?? 'Authentication required before starting automation.'); setAuthError(result.error ?? 'Authentication required before starting automation.');
return; return;

View File

@@ -19,7 +19,7 @@ export async function GET(request: NextRequest) {
const presenter = new SponsorDashboardPresenter(); const presenter = new SponsorDashboardPresenter();
const useCase = getGetSponsorDashboardUseCase(); const useCase = getGetSponsorDashboardUseCase();
await useCase.execute({ sponsorId }); await useCase.execute({ sponsorId }, presenter);
const dashboard = presenter.getData(); const dashboard = presenter.getData();
if (!dashboard) { if (!dashboard) {

View File

@@ -19,7 +19,7 @@ export async function GET(request: NextRequest) {
const presenter = new SponsorSponsorshipsPresenter(); const presenter = new SponsorSponsorshipsPresenter();
const useCase = getGetSponsorSponsorshipsUseCase(); const useCase = getGetSponsorSponsorshipsUseCase();
await useCase.execute({ sponsorId }); await useCase.execute({ sponsorId }, presenter);
const sponsorships = presenter.getData(); const sponsorships = presenter.getData();
if (!sponsorships) { if (!sponsorships) {

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, FormEvent } from 'react'; import { useState, FormEvent, type ChangeEvent } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { import {
@@ -145,7 +145,7 @@ export default function LoginPage() {
id="email" id="email"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, email: e.target.value })}
error={!!errors.email} error={!!errors.email}
errorMessage={errors.email} errorMessage={errors.email}
placeholder="you@example.com" placeholder="you@example.com"
@@ -172,7 +172,7 @@ export default function LoginPage() {
id="password" id="password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, password: e.target.value })}
error={!!errors.password} error={!!errors.password}
errorMessage={errors.password} errorMessage={errors.password}
placeholder="••••••••" placeholder="••••••••"

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, FormEvent } from 'react'; import { useState, useEffect, FormEvent, type ChangeEvent } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { import {
@@ -226,7 +226,7 @@ export default function SignupPage() {
id="displayName" id="displayName"
type="text" type="text"
value={formData.displayName} value={formData.displayName}
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })} onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, displayName: e.target.value })}
error={!!errors.displayName} error={!!errors.displayName}
errorMessage={errors.displayName} errorMessage={errors.displayName}
placeholder="SpeedyRacer42" placeholder="SpeedyRacer42"
@@ -249,7 +249,7 @@ export default function SignupPage() {
id="email" id="email"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, email: e.target.value })}
error={!!errors.email} error={!!errors.email}
errorMessage={errors.email} errorMessage={errors.email}
placeholder="you@example.com" placeholder="you@example.com"
@@ -271,7 +271,7 @@ export default function SignupPage() {
id="password" id="password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, password: e.target.value })}
error={!!errors.password} error={!!errors.password}
errorMessage={errors.password} errorMessage={errors.password}
placeholder="••••••••" placeholder="••••••••"
@@ -336,7 +336,7 @@ export default function SignupPage() {
id="confirmPassword" id="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'} type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })} onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, confirmPassword: e.target.value })}
error={!!errors.confirmPassword} error={!!errors.confirmPassword}
errorMessage={errors.confirmPassword} errorMessage={errors.confirmPassword}
placeholder="••••••••" placeholder="••••••••"

View File

@@ -27,6 +27,7 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import { getAuthService } from '@/lib/auth'; import { getAuthService } from '@/lib/auth';
import { getGetDashboardOverviewUseCase } from '@/lib/di-container'; import { getGetDashboardOverviewUseCase } from '@/lib/di-container';
import { DashboardOverviewPresenter } from '@/lib/presenters/DashboardOverviewPresenter';
import type { import type {
DashboardOverviewViewModel, DashboardOverviewViewModel,
DashboardFeedItemSummaryViewModel, DashboardFeedItemSummaryViewModel,
@@ -94,8 +95,9 @@ export default async function DashboardPage() {
const currentDriverId = session.user.primaryDriverId ?? ''; const currentDriverId = session.user.primaryDriverId ?? '';
const useCase = getGetDashboardOverviewUseCase(); const useCase = getGetDashboardOverviewUseCase();
await useCase.execute({ driverId: currentDriverId }); const presenter = new DashboardOverviewPresenter();
const viewModel = useCase.presenter.getViewModel() as DashboardOverviewViewModel | null; await useCase.execute({ driverId: currentDriverId }, presenter);
const viewModel = presenter.getViewModel();
if (!viewModel) { if (!viewModel) {
return null; return null;

View File

@@ -28,6 +28,7 @@ import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container'; import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import Image from 'next/image'; import Image from 'next/image';
@@ -387,13 +388,16 @@ export default function DriversPage() {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
const useCase = getGetDriversLeaderboardUseCase(); const useCase = getGetDriversLeaderboardUseCase();
await useCase.execute(); const presenter = new DriversLeaderboardPresenter();
const viewModel = useCase.presenter.getViewModel(); await useCase.execute(undefined as void, presenter);
const viewModel = presenter.getViewModel();
setDrivers(viewModel.drivers); if (viewModel) {
setTotalRaces(viewModel.totalRaces); setDrivers(viewModel.drivers);
setTotalWins(viewModel.totalWins); setTotalRaces(viewModel.totalRaces);
setActiveCount(viewModel.activeCount); setTotalWins(viewModel.totalWins);
setActiveCount(viewModel.activeCount);
}
setLoading(false); setLoading(false);
}; };

View File

@@ -20,6 +20,7 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container'; import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import Image from 'next/image'; import Image from 'next/image';
@@ -181,9 +182,12 @@ export default function DriverLeaderboardPage() {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
const useCase = getGetDriversLeaderboardUseCase(); const useCase = getGetDriversLeaderboardUseCase();
await useCase.execute(); const presenter = new DriversLeaderboardPresenter();
const viewModel = useCase.presenter.getViewModel(); await useCase.execute(undefined as void, presenter);
setDrivers(viewModel.drivers); const viewModel = presenter.getViewModel();
if (viewModel) {
setDrivers(viewModel.drivers);
}
setLoading(false); setLoading(false);
}; };

View File

@@ -21,6 +21,7 @@ import {
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { getGetDriversLeaderboardUseCase, getGetTeamsLeaderboardUseCase } from '@/lib/di-container'; import { getGetDriversLeaderboardUseCase, getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter';
import { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter'; import { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import type { TeamLeaderboardItemViewModel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter'; import type { TeamLeaderboardItemViewModel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
@@ -287,15 +288,16 @@ export default function LeaderboardsPage() {
try { try {
const driversUseCase = getGetDriversLeaderboardUseCase(); const driversUseCase = getGetDriversLeaderboardUseCase();
const teamsUseCase = getGetTeamsLeaderboardUseCase(); const teamsUseCase = getGetTeamsLeaderboardUseCase();
const driversPresenter = new DriversLeaderboardPresenter();
const teamsPresenter = new TeamsLeaderboardPresenter(); const teamsPresenter = new TeamsLeaderboardPresenter();
await driversUseCase.execute(); await driversUseCase.execute(undefined as void, driversPresenter);
await teamsUseCase.execute(undefined as void, teamsPresenter); await teamsUseCase.execute(undefined as void, teamsPresenter);
const driversViewModel = driversUseCase.presenter.getViewModel(); const driversViewModel = driversPresenter.getViewModel();
const teamsViewModel = teamsPresenter.getViewModel(); const teamsViewModel = teamsPresenter.getViewModel();
setDrivers(driversViewModel.drivers); setDrivers(driversViewModel?.drivers ?? []);
setTeams(teamsViewModel ? teamsViewModel.teams : []); setTeams(teamsViewModel ? teamsViewModel.teams : []);
} catch (error) { } catch (error) {
console.error('Failed to load leaderboard data:', error); console.error('Failed to load leaderboard data:', error);

View File

@@ -33,6 +33,7 @@ import {
getSponsorRepository, getSponsorRepository,
getSeasonSponsorshipRepository, getSeasonSponsorshipRepository,
} from '@/lib/di-container'; } from '@/lib/di-container';
import { LeagueScoringConfigPresenter } from '@/lib/presenters/LeagueScoringConfigPresenter';
import { Trophy, Star, ExternalLink } from 'lucide-react'; import { Trophy, Star, ExternalLink } from 'lucide-react';
import { getMembership, getLeagueMembers } from '@/lib/leagueMembership'; import { getMembership, getLeagueMembers } from '@/lib/leagueMembership';
import { useEffectiveDriverId } from '@/lib/currentDriver'; import { useEffectiveDriverId } from '@/lib/currentDriver';
@@ -125,8 +126,9 @@ export default function LeagueDetailPage() {
// Load scoring configuration for the active season // Load scoring configuration for the active season
const getLeagueScoringConfigUseCase = getGetLeagueScoringConfigUseCase(); const getLeagueScoringConfigUseCase = getGetLeagueScoringConfigUseCase();
await getLeagueScoringConfigUseCase.execute({ leagueId }); const scoringPresenter = new LeagueScoringConfigPresenter();
const scoringViewModel = getLeagueScoringConfigUseCase.presenter.getViewModel(); await getLeagueScoringConfigUseCase.execute({ leagueId }, scoringPresenter);
const scoringViewModel = scoringPresenter.getViewModel();
setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO); setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO);
// Load all drivers for standings and map to DTOs for UI components // Load all drivers for standings and map to DTOs for UI components

View File

@@ -7,6 +7,7 @@ import {
getLeagueRepository, getLeagueRepository,
getGetLeagueScoringConfigUseCase getGetLeagueScoringConfigUseCase
} from '@/lib/di-container'; } from '@/lib/di-container';
import { LeagueScoringConfigPresenter } from '@/lib/presenters/LeagueScoringConfigPresenter';
import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO'; import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO';
import type { League } from '@gridpilot/racing/domain/entities/League'; import type { League } from '@gridpilot/racing/domain/entities/League';
@@ -35,8 +36,9 @@ export default function LeagueRulebookPage() {
setLeague(leagueData); setLeague(leagueData);
await scoringUseCase.execute({ leagueId }); const scoringPresenter = new LeagueScoringConfigPresenter();
const scoringViewModel = scoringUseCase.presenter.getViewModel(); await scoringUseCase.execute({ leagueId }, scoringPresenter);
const scoringViewModel = scoringPresenter.getViewModel();
setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO); setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO);
} catch (err) { } catch (err) {
console.error('Failed to load scoring config:', err); console.error('Failed to load scoring config:', err);

View File

@@ -14,6 +14,7 @@ import {
getDriverRepository, getDriverRepository,
getLeagueMembershipRepository getLeagueMembershipRepository
} from '@/lib/di-container'; } from '@/lib/di-container';
import { LeagueDriverSeasonStatsPresenter } from '@/lib/presenters/LeagueDriverSeasonStatsPresenter';
import { useEffectiveDriverId } from '@/lib/currentDriver'; import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import type { MembershipRole, LeagueMembership } from '@/lib/leagueMembership'; import type { MembershipRole, LeagueMembership } from '@/lib/leagueMembership';
@@ -36,15 +37,9 @@ export default function LeagueStandingsPage() {
const driverRepo = getDriverRepository(); const driverRepo = getDriverRepository();
const membershipRepo = getLeagueMembershipRepository(); const membershipRepo = getLeagueMembershipRepository();
await getLeagueDriverSeasonStatsUseCase.execute({ leagueId }); const presenter = new LeagueDriverSeasonStatsPresenter();
type GetLeagueDriverSeasonStatsUseCaseType = { await getLeagueDriverSeasonStatsUseCase.execute({ leagueId }, presenter);
presenter: { const standingsViewModel = presenter.getViewModel();
getViewModel(): { stats: LeagueDriverSeasonStatsDTO[] };
};
};
const typedUseCase =
getLeagueDriverSeasonStatsUseCase as GetLeagueDriverSeasonStatsUseCaseType;
const standingsViewModel = typedUseCase.presenter.getViewModel();
setStandings(standingsViewModel.stats); setStandings(standingsViewModel.stats);
const allDrivers = await driverRepo.findAll(); const allDrivers = await driverRepo.findAll();

View File

@@ -30,7 +30,8 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; 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'; import { getGetAllLeaguesWithCapacityAndScoringUseCase } from '@/lib/di-container';
// ============================================================================ // ============================================================================
@@ -57,7 +58,7 @@ interface Category {
label: string; label: string;
icon: React.ElementType; icon: React.ElementType;
description: string; description: string;
filter: (league: LeagueSummaryDTO) => boolean; filter: (league: LeagueSummaryViewModel) => boolean;
color?: string; color?: string;
} }
@@ -175,7 +176,7 @@ interface LeagueSliderProps {
title: string; title: string;
icon: React.ElementType; icon: React.ElementType;
description: string; description: string;
leagues: LeagueSummaryDTO[]; leagues: LeagueSummaryViewModel[];
onLeagueClick: (id: string) => void; onLeagueClick: (id: string) => void;
autoScroll?: boolean; autoScroll?: boolean;
iconColor?: string; iconColor?: string;
@@ -377,25 +378,23 @@ function LeagueSlider({
export default function LeaguesPage() { export default function LeaguesPage() {
const router = useRouter(); const router = useRouter();
const [realLeagues, setRealLeagues] = useState<LeagueSummaryDTO[]>([]); const [realLeagues, setRealLeagues] = useState<LeagueSummaryViewModel[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [activeCategory, setActiveCategory] = useState<CategoryId>('all'); const [activeCategory, setActiveCategory] = useState<CategoryId>('all');
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
useEffect(() => { useEffect(() => {
loadLeagues(); void loadLeagues();
}, []); }, []);
const loadLeagues = async () => { const loadLeagues = async () => {
try { try {
const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase(); const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase();
await useCase.execute(); const presenter = new AllLeaguesWithCapacityAndScoringPresenter();
const presenter = useCase.presenter as unknown as { await useCase.execute(undefined as void, presenter);
getViewModel(): { leagues: LeagueSummaryDTO[] };
};
const viewModel = presenter.getViewModel(); const viewModel = presenter.getViewModel();
setRealLeagues(viewModel.leagues); setRealLeagues(viewModel?.leagues ?? []);
} catch (error) { } catch (error) {
console.error('Failed to load leagues:', error); console.error('Failed to load leagues:', error);
} finally { } finally {
@@ -434,7 +433,7 @@ export default function LeaguesPage() {
acc[category.id] = searchFilteredLeagues.filter(category.filter); acc[category.id] = searchFilteredLeagues.filter(category.filter);
return acc; return acc;
}, },
{} as Record<CategoryId, LeagueSummaryDTO[]>, {} as Record<CategoryId, LeagueSummaryViewModel[]>,
); );
// Featured categories to show as sliders with different scroll speeds and alternating directions // Featured categories to show as sliders with different scroll speeds and alternating directions

View File

@@ -34,6 +34,7 @@ import {
ArrowLeft, ArrowLeft,
Scale, Scale,
} from 'lucide-react'; } from 'lucide-react';
import { RaceDetailPresenter } from '@/lib/presenters/RaceDetailPresenter';
export default function RaceDetailPage() { export default function RaceDetailPage() {
const router = useRouter(); const router = useRouter();
@@ -57,8 +58,9 @@ export default function RaceDetailPage() {
setError(null); setError(null);
try { try {
const useCase = getGetRaceDetailUseCase(); const useCase = getGetRaceDetailUseCase();
await useCase.execute({ raceId, driverId: currentDriverId }); const presenter = new RaceDetailPresenter();
const vm = useCase.presenter.getViewModel(); await useCase.execute({ raceId, driverId: currentDriverId }, presenter);
const vm = presenter.getViewModel();
if (!vm) { if (!vm) {
throw new Error('Race detail not available'); throw new Error('Race detail not available');
} }

View File

@@ -13,6 +13,8 @@ import {
getGetRaceResultsDetailUseCase, getGetRaceResultsDetailUseCase,
getImportRaceResultsUseCase, getImportRaceResultsUseCase,
} from '@/lib/di-container'; } from '@/lib/di-container';
import { RaceWithSOFPresenter } from '@/lib/presenters/RaceWithSOFPresenter';
import { RaceResultsDetailPresenter } from '@/lib/presenters/RaceResultsDetailPresenter';
import type { import type {
RaceResultsHeaderViewModel, RaceResultsHeaderViewModel,
RaceResultsLeagueViewModel, RaceResultsLeagueViewModel,
@@ -71,7 +73,7 @@ export default function RaceResultsPage() {
const [currentDriverId, setCurrentDriverId] = useState<string | undefined>(undefined); const [currentDriverId, setCurrentDriverId] = useState<string | undefined>(undefined);
const [raceSOF, setRaceSOF] = useState<number | null>(null); const [raceSOF, setRaceSOF] = useState<number | null>(null);
const [penalties, setPenalties] = useState<PenaltyData[]>([]); const [penalties, setPenalties] = useState<PenaltyData[]>([]);
const [pointsSystem, setPointsSystem] = useState<Record<number, number>>({}); const [pointsSystem, setPointsSystem] = useState<Record<number, number> | undefined>(undefined);
const [fastestLapTime, setFastestLapTime] = useState<number | undefined>(undefined); const [fastestLapTime, setFastestLapTime] = useState<number | undefined>(undefined);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -81,9 +83,10 @@ export default function RaceResultsPage() {
const loadData = async () => { const loadData = async () => {
try { try {
const raceResultsUseCase = getGetRaceResultsDetailUseCase(); 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) { if (!viewModel) {
setError('Failed to load race data'); setError('Failed to load race data');
@@ -130,8 +133,9 @@ export default function RaceResultsPage() {
try { try {
const raceWithSOFUseCase = getGetRaceWithSOFUseCase(); const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
await raceWithSOFUseCase.execute({ raceId }); const sofPresenter = new RaceWithSOFPresenter();
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel(); await raceWithSOFUseCase.execute({ raceId }, sofPresenter);
const raceViewModel = sofPresenter.getViewModel();
if (raceViewModel) { if (raceViewModel) {
setRaceSOF(raceViewModel.strengthOfField); setRaceSOF(raceViewModel.strengthOfField);
} }
@@ -290,7 +294,7 @@ export default function RaceResultsPage() {
<ResultsTable <ResultsTable
results={results} results={results}
drivers={drivers} drivers={drivers}
pointsSystem={pointsSystem} pointsSystem={pointsSystem ?? {}}
fastestLapTime={fastestLapTime ?? 0} fastestLapTime={fastestLapTime ?? 0}
penalties={penalties} penalties={penalties}
currentDriverId={currentDriverId ?? ''} currentDriverId={currentDriverId ?? ''}

View File

@@ -8,6 +8,7 @@ import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import Breadcrumbs from '@/components/layout/Breadcrumbs'; import Breadcrumbs from '@/components/layout/Breadcrumbs';
import { getGetAllRacesPageDataUseCase } from '@/lib/di-container'; import { getGetAllRacesPageDataUseCase } from '@/lib/di-container';
import { AllRacesPagePresenter } from '@/lib/presenters/AllRacesPagePresenter';
import type { import type {
AllRacesPageViewModel, AllRacesPageViewModel,
AllRacesListItemViewModel, AllRacesListItemViewModel,
@@ -53,8 +54,9 @@ export default function AllRacesPage() {
const loadRaces = async () => { const loadRaces = async () => {
try { try {
const useCase = getGetAllRacesPageDataUseCase(); const useCase = getGetAllRacesPageDataUseCase();
await useCase.execute(); const presenter = new AllRacesPagePresenter();
const viewModel = useCase.presenter.getViewModel(); await useCase.execute(undefined, presenter);
const viewModel = presenter.getViewModel();
setPageData(viewModel); setPageData(viewModel);
} catch (err) { } catch (err) {
console.error('Failed to load races:', err); console.error('Failed to load races:', err);

View File

@@ -7,6 +7,7 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { getGetRacesPageDataUseCase } from '@/lib/di-container'; import { getGetRacesPageDataUseCase } from '@/lib/di-container';
import { RacesPagePresenter } from '@/lib/presenters/RacesPagePresenter';
import type { import type {
RacesPageViewModel, RacesPageViewModel,
RaceListItemViewModel, RaceListItemViewModel,
@@ -46,8 +47,9 @@ export default function RacesPage() {
const loadRaces = async () => { const loadRaces = async () => {
try { try {
const useCase = getGetRacesPageDataUseCase(); const useCase = getGetRacesPageDataUseCase();
await useCase.execute(); const presenter = new RacesPagePresenter();
const data = useCase.presenter.getViewModel(); await useCase.execute(undefined, presenter);
const data = presenter.getViewModel();
setPageData(data); setPageData(data);
} catch (err) { } catch (err) {
console.error('Failed to load races:', err); console.error('Failed to load races:', err);

View File

@@ -10,6 +10,8 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs';
import SponsorInsightsCard, { useSponsorMode, MetricBuilders, SlotTemplates } from '@/components/sponsors/SponsorInsightsCard'; import SponsorInsightsCard, { useSponsorMode, MetricBuilders, SlotTemplates } from '@/components/sponsors/SponsorInsightsCard';
import { getImageService } from '@/lib/di-container'; import { getImageService } from '@/lib/di-container';
import { TeamMembersPresenter } from '@/lib/presenters/TeamMembersPresenter'; 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 TeamRoster from '@/components/teams/TeamRoster';
import TeamStandings from '@/components/teams/TeamStandings'; import TeamStandings from '@/components/teams/TeamStandings';
import TeamAdmin from '@/components/teams/TeamAdmin'; import TeamAdmin from '@/components/teams/TeamAdmin';
@@ -20,7 +22,6 @@ import {
getTeamMembershipRepository, getTeamMembershipRepository,
} from '@/lib/di-container'; } from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver'; import { useEffectiveDriverId } from '@/lib/currentDriver';
import type { Team } from '@gridpilot/racing';
import { Users, Trophy, TrendingUp, Star, Zap } from 'lucide-react'; import { Users, Trophy, TrendingUp, Star, Zap } from 'lucide-react';
type TeamRole = 'owner' | 'manager' | 'driver'; type TeamRole = 'owner' | 'manager' | 'driver';
@@ -37,7 +38,9 @@ export default function TeamDetailPage() {
const params = useParams(); const params = useParams();
const teamId = params.id as string; const teamId = params.id as string;
const [team, setTeam] = useState<Team | null>(null); type TeamViewModel = TeamDetailsViewModel['team'];
const [team, setTeam] = useState<TeamViewModel | null>(null);
const [memberships, setMemberships] = useState<TeamMembership[]>([]); const [memberships, setMemberships] = useState<TeamMembership[]>([]);
const [activeTab, setActiveTab] = useState<Tab>('overview'); const [activeTab, setActiveTab] = useState<Tab>('overview');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -51,11 +54,9 @@ export default function TeamDetailPage() {
const detailsUseCase = getGetTeamDetailsUseCase(); const detailsUseCase = getGetTeamDetailsUseCase();
const membersUseCase = getGetTeamMembersUseCase(); const membersUseCase = getGetTeamMembersUseCase();
await detailsUseCase.execute(teamId, currentDriverId); const detailsPresenter = new TeamDetailsPresenter();
const detailsPresenter = detailsUseCase.presenter; await detailsUseCase.execute({ teamId, driverId: currentDriverId }, detailsPresenter);
const detailsViewModel = detailsPresenter const detailsViewModel = detailsPresenter.getViewModel();
? (detailsPresenter as any).getViewModel?.() as { team: Team } | null
: null;
if (!detailsViewModel) { if (!detailsViewModel) {
setTeam(null); setTeam(null);

View File

@@ -12,12 +12,12 @@ import {
ChevronRight, ChevronRight,
Sparkles, Sparkles,
} from 'lucide-react'; } 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 { getLeagueCoverClasses } from '@/lib/leagueCovers';
import { getImageService } from '@/lib/di-container'; import { getImageService } from '@/lib/di-container';
interface LeagueCardProps { interface LeagueCardProps {
league: LeagueSummaryDTO; league: LeagueSummaryViewModel;
onClick?: () => void; 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(); const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
return new Date(createdAt) > oneWeekAgo; return new Date(createdAt) > oneWeekAgo;

View File

@@ -251,7 +251,7 @@ export default function DriverProfileMockup() {
function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boolean; value: number }) { function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boolean; value: number }) {
const count = useMotionValue(0); 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 }); const spring = useSpring(count, { stiffness: 50, damping: 25 });
useEffect(() => { useEffect(() => {
@@ -282,7 +282,7 @@ function AnimatedCounter({
suffix?: string; suffix?: string;
}) { }) {
const count = useMotionValue(0); const count = useMotionValue(0);
const rounded = useTransform(count, (v) => Math.round(v)); const rounded = useTransform(count, (v: number) => Math.round(v));
useEffect(() => { useEffect(() => {
if (shouldReduceMotion) { if (shouldReduceMotion) {

View File

@@ -146,7 +146,7 @@ function RatingFactor({
}) { }) {
const progress = useMotionValue(0); const progress = useMotionValue(0);
const smoothProgress = useSpring(progress, { stiffness: 60, damping: 25 }); const smoothProgress = useSpring(progress, { stiffness: 60, damping: 25 });
const width = useTransform(smoothProgress, (v) => `${v}%`); const width = useTransform(smoothProgress, (v: number) => `${v}%`);
useEffect(() => { useEffect(() => {
if (shouldReduceMotion) { if (shouldReduceMotion) {
@@ -187,7 +187,7 @@ function RatingFactor({
function AnimatedRating({ shouldReduceMotion }: { shouldReduceMotion: boolean }) { function AnimatedRating({ shouldReduceMotion }: { shouldReduceMotion: boolean }) {
const count = useMotionValue(0); 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 }); const spring = useSpring(count, { stiffness: 50, damping: 25 });
useEffect(() => { useEffect(() => {

49
apps/website/env.d.ts vendored
View File

@@ -1,6 +1,55 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
declare module 'framer-motion' {
import type { ComponentType } from 'react';
// Minimal shim to satisfy strict typing for usage in JSX
export type MotionComponent = ComponentType<Record<string, unknown>>;
export const motion: {
div: MotionComponent;
span: MotionComponent;
button: MotionComponent;
svg: MotionComponent;
p: MotionComponent;
[element: string]: MotionComponent;
};
export const AnimatePresence: ComponentType<Record<string, unknown>>;
export function useReducedMotion(): boolean;
// Shim motion values with a minimal interface exposing .set()
export interface MotionValue<T = number> {
get(): T;
set(v: T): void;
}
export function useMotionValue(initial: number): MotionValue<number>;
export function useSpring(value: MotionValue<number> | number, config?: Record<string, unknown>): MotionValue<number>;
export function useTransform<TInput, TOutput>(
value: MotionValue<TInput>,
transformer: (input: TInput) => TOutput,
): MotionValue<TOutput>;
}
declare module '@next/third-party-devtools' {
import type { ComponentType } from 'react';
const Devtools: ComponentType<Record<string, unknown>>;
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<T = unknown> = (action: T) => void;
}
declare namespace NodeJS { declare namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha'; NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha';

View File

@@ -195,15 +195,15 @@ export function configureDIContainer(): void {
// Create driver statistics from seed data // Create driver statistics from seed data
type DemoDriverStatsEntry = { type DemoDriverStatsEntry = {
rating?: number; rating: number;
wins?: number; wins: number;
podiums?: number; podiums: number;
totalRaces: number;
overallRank: number | null;
dnfs?: number; dnfs?: number;
totalRaces?: number;
avgFinish?: number; avgFinish?: number;
bestFinish?: number; bestFinish?: number;
worstFinish?: number; worstFinish?: number;
overallRank?: number;
consistency?: number; consistency?: number;
percentile?: number; percentile?: number;
driverId?: string; driverId?: string;
@@ -952,10 +952,9 @@ export function configureDIContainer(): void {
new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter) new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter)
); );
const raceRegistrationsPresenter = new RaceRegistrationsPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetRaceRegistrationsUseCase, DI_TOKENS.GetRaceRegistrationsUseCase,
new GetRaceRegistrationsUseCase(raceRegistrationRepository, raceRegistrationsPresenter) new GetRaceRegistrationsUseCase(raceRegistrationRepository)
); );
const leagueStandingsPresenter = new LeagueStandingsPresenter(); const leagueStandingsPresenter = new LeagueStandingsPresenter();
@@ -964,7 +963,6 @@ export function configureDIContainer(): void {
new GetLeagueStandingsUseCase(standingRepository), new GetLeagueStandingsUseCase(standingRepository),
); );
const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetLeagueDriverSeasonStatsUseCase, DI_TOKENS.GetLeagueDriverSeasonStatsUseCase,
new GetLeagueDriverSeasonStatsUseCase( new GetLeagueDriverSeasonStatsUseCase(
@@ -986,21 +984,17 @@ export function configureDIContainer(): void {
}; };
}, },
}, },
leagueDriverSeasonStatsPresenter,
), ),
); );
const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetAllLeaguesWithCapacityUseCase, DI_TOKENS.GetAllLeaguesWithCapacityUseCase,
new GetAllLeaguesWithCapacityUseCase( new GetAllLeaguesWithCapacityUseCase(
leagueRepository, leagueRepository,
leagueMembershipRepository, leagueMembershipRepository,
allLeaguesWithCapacityPresenter
) )
); );
const allLeaguesWithCapacityAndScoringPresenter = new AllLeaguesWithCapacityAndScoringPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase, DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase,
new GetAllLeaguesWithCapacityAndScoringUseCase( new GetAllLeaguesWithCapacityAndScoringUseCase(
@@ -1010,7 +1004,6 @@ export function configureDIContainer(): void {
leagueScoringConfigRepository, leagueScoringConfigRepository,
gameRepository, gameRepository,
leagueScoringPresetProvider, leagueScoringPresetProvider,
allLeaguesWithCapacityAndScoringPresenter
) )
); );
@@ -1020,7 +1013,6 @@ export function configureDIContainer(): void {
new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider) new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider)
); );
const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetLeagueScoringConfigUseCase, DI_TOKENS.GetLeagueScoringConfigUseCase,
new GetLeagueScoringConfigUseCase( new GetLeagueScoringConfigUseCase(
@@ -1029,7 +1021,6 @@ export function configureDIContainer(): void {
leagueScoringConfigRepository, leagueScoringConfigRepository,
gameRepository, gameRepository,
leagueScoringPresetProvider, leagueScoringPresetProvider,
leagueScoringConfigPresenter
) )
); );
@@ -1049,7 +1040,6 @@ export function configureDIContainer(): void {
new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter), new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter),
); );
const raceWithSOFPresenter = new RaceWithSOFPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetRaceWithSOFUseCase, DI_TOKENS.GetRaceWithSOFUseCase,
new GetRaceWithSOFUseCase( new GetRaceWithSOFUseCase(
@@ -1057,7 +1047,6 @@ export function configureDIContainer(): void {
raceRegistrationRepository, raceRegistrationRepository,
resultRepository, resultRepository,
driverRatingProvider, driverRatingProvider,
raceWithSOFPresenter
) )
); );
@@ -1073,21 +1062,18 @@ export function configureDIContainer(): void {
) )
); );
const racesPresenter = new RacesPagePresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetRacesPageDataUseCase, DI_TOKENS.GetRacesPageDataUseCase,
new GetRacesPageDataUseCase(raceRepository, leagueRepository, racesPresenter) new GetRacesPageDataUseCase(raceRepository, leagueRepository)
); );
const allRacesPagePresenter = new AllRacesPagePresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetAllRacesPageDataUseCase, DI_TOKENS.GetAllRacesPageDataUseCase,
new GetAllRacesPageDataUseCase(raceRepository, leagueRepository, allRacesPagePresenter) new GetAllRacesPageDataUseCase(raceRepository, leagueRepository)
); );
const imageService = container.resolve<ImageServicePort>(DI_TOKENS.ImageService); const imageService = container.resolve<ImageServicePort>(DI_TOKENS.ImageService);
const raceDetailPresenter = new RaceDetailPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetRaceDetailUseCase, DI_TOKENS.GetRaceDetailUseCase,
new GetRaceDetailUseCase( new GetRaceDetailUseCase(
@@ -1099,11 +1085,9 @@ export function configureDIContainer(): void {
leagueMembershipRepository, leagueMembershipRepository,
driverRatingProvider, driverRatingProvider,
imageService, imageService,
raceDetailPresenter
) )
); );
const raceResultsDetailPresenter = new RaceResultsDetailPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetRaceResultsDetailUseCase, DI_TOKENS.GetRaceResultsDetailUseCase,
new GetRaceResultsDetailUseCase( new GetRaceResultsDetailUseCase(
@@ -1112,7 +1096,6 @@ export function configureDIContainer(): void {
resultRepository, resultRepository,
driverRepository, driverRepository,
penaltyRepository, penaltyRepository,
raceResultsDetailPresenter
) )
); );
@@ -1149,7 +1132,6 @@ export function configureDIContainer(): void {
}, },
}; };
const driversPresenter = new DriversLeaderboardPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetDriversLeaderboardUseCase, DI_TOKENS.GetDriversLeaderboardUseCase,
new GetDriversLeaderboardUseCase( new GetDriversLeaderboardUseCase(
@@ -1157,7 +1139,6 @@ export function configureDIContainer(): void {
rankingService, rankingService,
driverStatsService, driverStatsService,
imageService, imageService,
driversPresenter
) )
); );
@@ -1215,7 +1196,6 @@ export function configureDIContainer(): void {
}; };
}; };
const dashboardOverviewPresenter = new DashboardOverviewPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetDashboardOverviewUseCase, DI_TOKENS.GetDashboardOverviewUseCase,
new GetDashboardOverviewUseCase( new GetDashboardOverviewUseCase(
@@ -1230,7 +1210,6 @@ export function configureDIContainer(): void {
socialRepository, socialRepository,
imageService, imageService,
getDriverStatsForDashboard, getDriverStatsForDashboard,
dashboardOverviewPresenter
) )
); );
@@ -1261,10 +1240,9 @@ export function configureDIContainer(): void {
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository), new GetAllTeamsUseCase(teamRepository, teamMembershipRepository),
); );
const teamDetailsPresenter = new TeamDetailsPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetTeamDetailsUseCase, DI_TOKENS.GetTeamDetailsUseCase,
new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository, teamDetailsPresenter) new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository)
); );
const teamMembersPresenter = new TeamMembersPresenter(); const teamMembersPresenter = new TeamMembersPresenter();
@@ -1313,7 +1291,6 @@ export function configureDIContainer(): void {
const sponsorRepository = container.resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository); const sponsorRepository = container.resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository);
const seasonSponsorshipRepository = container.resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository); const seasonSponsorshipRepository = container.resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
const sponsorDashboardPresenter = new SponsorDashboardPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetSponsorDashboardUseCase, DI_TOKENS.GetSponsorDashboardUseCase,
new GetSponsorDashboardUseCase( new GetSponsorDashboardUseCase(
@@ -1323,11 +1300,9 @@ export function configureDIContainer(): void {
leagueRepository, leagueRepository,
leagueMembershipRepository, leagueMembershipRepository,
raceRepository, raceRepository,
sponsorDashboardPresenter
) )
); );
const sponsorSponsorshipsPresenter = new SponsorSponsorshipsPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetSponsorSponsorshipsUseCase, DI_TOKENS.GetSponsorSponsorshipsUseCase,
new GetSponsorSponsorshipsUseCase( new GetSponsorSponsorshipsUseCase(
@@ -1337,7 +1312,6 @@ export function configureDIContainer(): void {
leagueRepository, leagueRepository,
leagueMembershipRepository, leagueMembershipRepository,
raceRepository, raceRepository,
sponsorSponsorshipsPresenter
) )
); );

View File

@@ -625,7 +625,7 @@ export function getIsDriverRegisteredForRaceQuery(): {
return { return {
async execute(input: { raceId: string; driverId: string }): Promise<boolean> { async execute(input: { raceId: string; driverId: string }): Promise<boolean> {
const result = await useCase.execute(input); const result = await useCase.execute(input);
return result as unknown as boolean; return Boolean(result);
}, },
}; };
} }

View File

@@ -1,4 +1,3 @@
import type { League } from '@gridpilot/racing/domain/entities/League';
import type { import type {
IAllLeaguesWithCapacityAndScoringPresenter, IAllLeaguesWithCapacityAndScoringPresenter,
LeagueEnrichedData, LeagueEnrichedData,
@@ -9,7 +8,11 @@ import type {
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter { export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null; 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 leagueItems: LeagueSummaryViewModel[] = enrichedLeagues.map((data) => {
const { league, usedDriverSlots, season, scoringConfig, game, preset } = data; const { league, usedDriverSlots, season, scoringConfig, game, preset } = data;
@@ -68,7 +71,7 @@ export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWit
name: league.name, name: league.name,
description: league.description, description: league.description,
ownerId: league.ownerId, ownerId: league.ownerId,
createdAt: league.createdAt, createdAt: league.createdAt.toISOString(),
maxDrivers: safeMaxDrivers, maxDrivers: safeMaxDrivers,
usedDriverSlots, usedDriverSlots,
// Team capacity is not yet modeled here; use zero for now to satisfy strict typing. // 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, leagues: leagueItems,
totalCount: leagueItems.length, totalCount: leagueItems.length,
}; };
return this.viewModel;
} }
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel { getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel; return this.viewModel;
} }

View File

@@ -1,17 +1,19 @@
import type { League } from '@gridpilot/racing/domain/entities/League';
import type { import type {
IAllLeaguesWithCapacityPresenter, IAllLeaguesWithCapacityPresenter,
LeagueWithCapacityViewModel, LeagueWithCapacityViewModel,
AllLeaguesWithCapacityViewModel, AllLeaguesWithCapacityViewModel,
AllLeaguesWithCapacityResultDTO,
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter'; } from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter { export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter {
private viewModel: AllLeaguesWithCapacityViewModel | null = null; private viewModel: AllLeaguesWithCapacityViewModel | null = null;
present( reset(): void {
leagues: League[], this.viewModel = null;
memberCounts: Map<string, number> }
): AllLeaguesWithCapacityViewModel {
present(input: AllLeaguesWithCapacityResultDTO): void {
const { leagues, memberCounts } = input;
const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => { const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => {
const usedSlots = memberCounts.get(league.id) ?? 0; const usedSlots = memberCounts.get(league.id) ?? 0;
@@ -63,14 +65,9 @@ export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityP
leagues: leagueItems, leagues: leagueItems,
totalCount: leagueItems.length, totalCount: leagueItems.length,
}; };
return this.viewModel;
} }
getViewModel(): AllLeaguesWithCapacityViewModel { getViewModel(): AllLeaguesWithCapacityViewModel | null {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel; return this.viewModel;
} }
} }

View File

@@ -1,13 +1,18 @@
import type { import type {
IAllRacesPagePresenter, IAllRacesPagePresenter,
AllRacesPageResultDTO,
AllRacesPageViewModel, AllRacesPageViewModel,
} from '@gridpilot/racing/application/presenters/IAllRacesPagePresenter'; } from '@gridpilot/racing/application/presenters/IAllRacesPagePresenter';
export class AllRacesPagePresenter implements IAllRacesPagePresenter { export class AllRacesPagePresenter implements IAllRacesPagePresenter {
private viewModel: AllRacesPageViewModel | null = null; private viewModel: AllRacesPageViewModel | null = null;
present(viewModel: AllRacesPageViewModel): void { reset(): void {
this.viewModel = viewModel; this.viewModel = null;
}
present(dto: AllRacesPageResultDTO): void {
this.viewModel = dto;
} }
getViewModel(): AllRacesPageViewModel | null { getViewModel(): AllRacesPageViewModel | null {

View File

@@ -1,13 +1,18 @@
import type { import type {
IDashboardOverviewPresenter, IDashboardOverviewPresenter,
DashboardOverviewResultDTO,
DashboardOverviewViewModel, DashboardOverviewViewModel,
} from '@gridpilot/racing/application/presenters/IDashboardOverviewPresenter'; } from '@gridpilot/racing/application/presenters/IDashboardOverviewPresenter';
export class DashboardOverviewPresenter implements IDashboardOverviewPresenter { export class DashboardOverviewPresenter implements IDashboardOverviewPresenter {
private viewModel: DashboardOverviewViewModel | null = null; private viewModel: DashboardOverviewViewModel | null = null;
present(viewModel: DashboardOverviewViewModel): void { reset(): void {
this.viewModel = viewModel; this.viewModel = null;
}
present(dto: DashboardOverviewResultDTO): void {
this.viewModel = dto;
} }
getViewModel(): DashboardOverviewViewModel | null { getViewModel(): DashboardOverviewViewModel | null {

View File

@@ -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 { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
import type { import type {
IDriversLeaderboardPresenter, IDriversLeaderboardPresenter,
DriverLeaderboardItemViewModel, DriverLeaderboardItemViewModel,
DriversLeaderboardViewModel, DriversLeaderboardViewModel,
DriversLeaderboardResultDTO,
} from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter'; } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter { export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
private viewModel: DriversLeaderboardViewModel | null = null; private viewModel: DriversLeaderboardViewModel | null = null;
present( reset(): void {
drivers: Driver[], this.viewModel = null;
rankings: Array<{ driverId: string; rating: number; overallRank: number }>, }
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
avatarUrls: Record<string, string> present(input: DriversLeaderboardResultDTO): void {
): DriversLeaderboardViewModel { const { drivers, rankings, stats, avatarUrls } = input;
const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => { const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => {
const driverStats = stats[driver.id]; const driverStats = stats[driver.id];
const rating = driverStats?.rating ?? 0; const rating = driverStats?.rating ?? 0;
@@ -68,14 +67,9 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter
totalWins, totalWins,
activeCount, activeCount,
}; };
return this.viewModel;
} }
getViewModel(): DriversLeaderboardViewModel { getViewModel(): DriversLeaderboardViewModel | null {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel; return this.viewModel;
} }
} }

View File

@@ -2,23 +2,19 @@ import type {
ILeagueDriverSeasonStatsPresenter, ILeagueDriverSeasonStatsPresenter,
LeagueDriverSeasonStatsItemViewModel, LeagueDriverSeasonStatsItemViewModel,
LeagueDriverSeasonStatsViewModel, LeagueDriverSeasonStatsViewModel,
LeagueDriverSeasonStatsResultDTO,
} from '@gridpilot/racing/application/presenters/ILeagueDriverSeasonStatsPresenter'; } from '@gridpilot/racing/application/presenters/ILeagueDriverSeasonStatsPresenter';
export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter { export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter {
private viewModel: LeagueDriverSeasonStatsViewModel | null = null; private viewModel: LeagueDriverSeasonStatsViewModel | null = null;
present( reset(): void {
leagueId: string, this.viewModel = null;
standings: Array<{ }
driverId: string;
position: number; present(dto: LeagueDriverSeasonStatsResultDTO): void {
points: number; const { leagueId, standings, penalties, driverResults, driverRatings } = dto;
racesCompleted: number;
}>,
penalties: Map<string, { baseDelta: number; bonusDelta: number }>,
driverResults: Map<string, Array<{ position: number }>>,
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
): LeagueDriverSeasonStatsViewModel {
const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => { const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => {
const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 }; const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
const totalPenaltyPoints = penalty.baseDelta; const totalPenaltyPoints = penalty.baseDelta;
@@ -65,8 +61,6 @@ export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStat
leagueId, leagueId,
stats, stats,
}; };
return this.viewModel;
} }
getViewModel(): LeagueDriverSeasonStatsViewModel { getViewModel(): LeagueDriverSeasonStatsViewModel {

View File

@@ -10,6 +10,10 @@ import type {
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter { export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
private viewModel: LeagueScoringConfigViewModel | null = null; private viewModel: LeagueScoringConfigViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel { present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
const championships: LeagueScoringChampionshipViewModel[] = const championships: LeagueScoringChampionshipViewModel[] =
data.championships.map((champ) => this.mapChampionship(champ)); data.championships.map((champ) => this.mapChampionship(champ));

View File

@@ -6,9 +6,13 @@ import type {
export class RaceDetailPresenter implements IRaceDetailPresenter { export class RaceDetailPresenter implements IRaceDetailPresenter {
private viewModel: RaceDetailViewModel | null = null; private viewModel: RaceDetailViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(viewModel: RaceDetailViewModel): RaceDetailViewModel { present(viewModel: RaceDetailViewModel): RaceDetailViewModel {
this.viewModel = viewModel; this.viewModel = viewModel;
return this.viewModel; return viewModel;
} }
getViewModel(): RaceDetailViewModel | null { getViewModel(): RaceDetailViewModel | null {

View File

@@ -1,24 +1,25 @@
import type { import type {
IRaceRegistrationsPresenter, IRaceRegistrationsPresenter,
RaceRegistrationsViewModel, RaceRegistrationsViewModel,
RaceRegistrationsResultDTO,
} from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter'; } from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter';
export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter { export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
private viewModel: RaceRegistrationsViewModel | null = null; private viewModel: RaceRegistrationsViewModel | null = null;
present(registeredDriverIds: string[]): RaceRegistrationsViewModel { reset(): void {
this.viewModel = null;
}
present(input: RaceRegistrationsResultDTO): void {
const { registeredDriverIds } = input;
this.viewModel = { this.viewModel = {
registeredDriverIds, registeredDriverIds,
count: registeredDriverIds.length, count: registeredDriverIds.length,
}; };
return this.viewModel;
} }
getViewModel(): RaceRegistrationsViewModel { getViewModel(): RaceRegistrationsViewModel | null {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel; return this.viewModel;
} }
} }

View File

@@ -6,9 +6,12 @@ import type {
export class RaceResultsDetailPresenter implements IRaceResultsDetailPresenter { export class RaceResultsDetailPresenter implements IRaceResultsDetailPresenter {
private viewModel: RaceResultsDetailViewModel | null = null; private viewModel: RaceResultsDetailViewModel | null = null;
present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel { reset(): void {
this.viewModel = null;
}
present(viewModel: RaceResultsDetailViewModel): void {
this.viewModel = viewModel; this.viewModel = viewModel;
return this.viewModel;
} }
getViewModel(): RaceResultsDetailViewModel | null { getViewModel(): RaceResultsDetailViewModel | null {

View File

@@ -1,49 +1,35 @@
import type { import type {
IRaceWithSOFPresenter, IRaceWithSOFPresenter,
RaceWithSOFResultDTO,
RaceWithSOFViewModel, RaceWithSOFViewModel,
} from '@gridpilot/racing/application/presenters/IRaceWithSOFPresenter'; } from '@gridpilot/racing/application/presenters/IRaceWithSOFPresenter';
export class RaceWithSOFPresenter implements IRaceWithSOFPresenter { export class RaceWithSOFPresenter implements IRaceWithSOFPresenter {
private viewModel: RaceWithSOFViewModel | null = null; private viewModel: RaceWithSOFViewModel | null = null;
present( present(dto: RaceWithSOFResultDTO): void {
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 {
this.viewModel = { this.viewModel = {
id: raceId, id: dto.raceId,
leagueId, leagueId: dto.leagueId,
scheduledAt: scheduledAt.toISOString(), scheduledAt: dto.scheduledAt.toISOString(),
track, track: dto.track,
trackId, trackId: dto.trackId,
car, car: dto.car,
carId, carId: dto.carId,
sessionType, sessionType: dto.sessionType,
status, status: dto.status,
strengthOfField, strengthOfField: dto.strengthOfField,
registeredCount, registeredCount: dto.registeredCount,
maxParticipants, maxParticipants: dto.maxParticipants,
participantCount, participantCount: dto.participantCount,
}; };
}
getViewModel(): RaceWithSOFViewModel | null {
return this.viewModel; return this.viewModel;
} }
getViewModel(): RaceWithSOFViewModel { reset(): void {
if (!this.viewModel) { this.viewModel = null;
throw new Error('Presenter has not been called yet');
}
return this.viewModel;
} }
} }

View File

@@ -2,26 +2,18 @@ import type {
IRacesPagePresenter, IRacesPagePresenter,
RacesPageViewModel, RacesPageViewModel,
RaceListItemViewModel, RaceListItemViewModel,
RacesPageResultDTO,
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter'; } 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 { export class RacesPagePresenter implements IRacesPagePresenter {
private viewModel: RacesPageViewModel | null = null; 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 now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);

View File

@@ -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'; import type { SponsorDashboardDTO } from '@gridpilot/racing/application/use-cases/GetSponsorDashboardUseCase';
export class SponsorDashboardPresenter implements ISponsorDashboardPresenter { export class SponsorDashboardPresenter implements ISponsorDashboardPresenter {
private data: SponsorDashboardDTO | null = null; private viewModel: SponsorDashboardViewModel = null;
reset(): void {
this.viewModel = null;
}
present(data: SponsorDashboardDTO | null): void { present(data: SponsorDashboardDTO | null): void {
this.data = data; this.viewModel = data;
}
getViewModel(): SponsorDashboardViewModel {
return this.viewModel;
} }
getData(): SponsorDashboardDTO | null { getData(): SponsorDashboardDTO | null {
return this.data; return this.viewModel;
} }
} }

View File

@@ -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'; import type { SponsorSponsorshipsDTO } from '@gridpilot/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
export class SponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter { export class SponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter {
private data: SponsorSponsorshipsDTO | null = null; private viewModel: SponsorSponsorshipsViewModel = null;
reset(): void {
this.viewModel = null;
}
present(data: SponsorSponsorshipsDTO | null): void { present(data: SponsorSponsorshipsDTO | null): void {
this.data = data; this.viewModel = data;
}
getViewModel(): SponsorSponsorshipsViewModel {
return this.viewModel;
} }
getData(): SponsorSponsorshipsDTO | null { getData(): SponsorSponsorshipsDTO | null {
return this.data; return this.viewModel;
} }
} }

View File

@@ -1,18 +1,18 @@
import type { Team } from '@gridpilot/racing/domain/entities/Team';
import type { TeamMembership } from '@gridpilot/racing/domain/types/TeamMembership';
import type { import type {
ITeamDetailsPresenter, ITeamDetailsPresenter,
TeamDetailsViewModel, TeamDetailsViewModel,
TeamDetailsResultDTO,
} from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter'; } from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
export class TeamDetailsPresenter implements ITeamDetailsPresenter { export class TeamDetailsPresenter implements ITeamDetailsPresenter {
private viewModel: TeamDetailsViewModel | null = null; private viewModel: TeamDetailsViewModel | null = null;
present( reset(): void {
team: Team, this.viewModel = null;
membership: TeamMembership | null, }
driverId: string
): TeamDetailsViewModel { present(input: TeamDetailsResultDTO): void {
const { team, membership } = input;
const canManage = membership?.role === 'owner' || membership?.role === 'manager'; const canManage = membership?.role === 'owner' || membership?.role === 'manager';
const viewModel: TeamDetailsViewModel = { const viewModel: TeamDetailsViewModel = {
@@ -23,6 +23,7 @@ export class TeamDetailsPresenter implements ITeamDetailsPresenter {
description: team.description, description: team.description,
ownerId: team.ownerId, ownerId: team.ownerId,
leagues: team.leagues, leagues: team.leagues,
createdAt: team.createdAt.toISOString(),
}, },
membership: membership membership: membership
? { ? {
@@ -35,14 +36,9 @@ export class TeamDetailsPresenter implements ITeamDetailsPresenter {
}; };
this.viewModel = viewModel; this.viewModel = viewModel;
return viewModel;
} }
getViewModel(): TeamDetailsViewModel { getViewModel(): TeamDetailsViewModel | null {
if (!this.viewModel) {
throw new Error('Presenter has not been called yet');
}
return this.viewModel; return this.viewModel;
} }
} }

View File

@@ -1,21 +1,11 @@
{ {
"extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "baseUrl": ".",
"isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
@@ -25,11 +15,19 @@
], ],
"paths": { "paths": {
"@/*": ["./*"], "@/*": ["./*"],
"@/lib/*": ["./lib/*"],
"@/components/*": ["./components/*"],
"@/app/*": ["./app/*"],
"@gridpilot/identity": ["../../packages/identity/index.ts"],
"@gridpilot/identity/*": ["../../packages/identity/*"], "@gridpilot/identity/*": ["../../packages/identity/*"],
"@gridpilot/racing": ["../../packages/racing/index.ts"],
"@gridpilot/racing/*": ["../../packages/racing/*"], "@gridpilot/racing/*": ["../../packages/racing/*"],
"@gridpilot/social": ["../../packages/social/index.ts"],
"@gridpilot/social/*": ["../../packages/social/*"], "@gridpilot/social/*": ["../../packages/social/*"],
"@gridpilot/testing-support": ["../../packages/testing-support"], "@gridpilot/testing-support": ["../../packages/testing-support/index.ts"],
"@gridpilot/media": ["../../packages/media"], "@gridpilot/testing-support/*": ["../../packages/testing-support/*"],
"@gridpilot/media": ["../../packages/media/index.ts"],
"@gridpilot/media/*": ["../../packages/media/*"],
"@gridpilot/shared": ["../../packages/shared/index.ts"], "@gridpilot/shared": ["../../packages/shared/index.ts"],
"@gridpilot/shared/application": ["../../packages/shared/application"], "@gridpilot/shared/application": ["../../packages/shared/application"],
"@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"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules", ".next"]
} }

View File

@@ -0,0 +1,4 @@
/**
* Intentionally left blank.
* Third-party shims are now defined in env.d.ts.
*/

View File

@@ -2,6 +2,7 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig'; import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId'; import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort'; import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort'; import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator'; import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
@@ -36,6 +37,34 @@ export class AutomationEngineAdapter implements AutomationEnginePort {
private readonly sessionRepository: SessionRepositoryPort private readonly sessionRepository: SessionRepositoryPort
) {} ) {}
private toStepConfig(config: HostedSessionConfig): Record<string, unknown> {
const baseConfig: Record<string, unknown> = {
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<ValidationResult> { async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {
if (!config.sessionName || config.sessionName.trim() === '') { if (!config.sessionName || config.sessionName.trim() === '') {
return { isValid: false, error: 'Session name is required' }; return { isValid: false, error: 'Session name is required' };
@@ -89,7 +118,7 @@ export class AutomationEngineAdapter implements AutomationEnginePort {
// Execute current step using the browser automation // Execute current step using the browser automation
if (this.browserAutomation.executeStep) { if (this.browserAutomation.executeStep) {
const result = await this.browserAutomation.executeStep(currentStep, config as unknown as Record<string, unknown>); const result = await this.browserAutomation.executeStep(currentStep, this.toStepConfig(config));
if (!result.success) { if (!result.success) {
const stepDescription = StepTransitionValidator.getStepDescription(currentStep); const stepDescription = StepTransitionValidator.getStepDescription(currentStep);
const errorMessage = `Step ${currentStep.value} (${stepDescription}) failed: ${result.error}`; const errorMessage = `Step ${currentStep.value} (${stepDescription}) failed: ${result.error}`;
@@ -117,7 +146,7 @@ export class AutomationEngineAdapter implements AutomationEnginePort {
if (nextStep.isFinalStep()) { if (nextStep.isFinalStep()) {
// Execute final step handler // Execute final step handler
if (this.browserAutomation.executeStep) { if (this.browserAutomation.executeStep) {
const result = await this.browserAutomation.executeStep(nextStep, config as unknown as Record<string, unknown>); const result = await this.browserAutomation.executeStep(nextStep, this.toStepConfig(config));
if (!result.success) { if (!result.success) {
const stepDescription = StepTransitionValidator.getStepDescription(nextStep); const stepDescription = StepTransitionValidator.getStepDescription(nextStep);
const errorMessage = `Step ${nextStep.value} (${stepDescription}) failed: ${result.error}`; const errorMessage = `Step ${nextStep.value} (${stepDescription}) failed: ${result.error}`;

View File

@@ -2,6 +2,7 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig'; import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId'; import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort'; import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort'; import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator'; import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
@@ -19,6 +20,34 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort {
private readonly sessionRepository: SessionRepositoryPort private readonly sessionRepository: SessionRepositoryPort
) {} ) {}
private toStepConfig(config: HostedSessionConfig): Record<string, unknown> {
const baseConfig: Record<string, unknown> = {
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<ValidationResult> { async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {
if (!config.sessionName || config.sessionName.trim() === '') { if (!config.sessionName || config.sessionName.trim() === '') {
return { isValid: false, error: 'Session name is required' }; return { isValid: false, error: 'Session name is required' };
@@ -74,7 +103,7 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort {
if (this.browserAutomation.executeStep) { if (this.browserAutomation.executeStep) {
const result = await this.browserAutomation.executeStep( const result = await this.browserAutomation.executeStep(
currentStep, currentStep,
config as unknown as Record<string, unknown>, this.toStepConfig(config),
); );
if (!result.success) { if (!result.success) {
const stepDescription = StepTransitionValidator.getStepDescription(currentStep); const stepDescription = StepTransitionValidator.getStepDescription(currentStep);
@@ -105,7 +134,7 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort {
if (this.browserAutomation.executeStep) { if (this.browserAutomation.executeStep) {
const result = await this.browserAutomation.executeStep( const result = await this.browserAutomation.executeStep(
nextStep, nextStep,
config as unknown as Record<string, unknown>, this.toStepConfig(config),
); );
if (!result.success) { if (!result.success) {
const stepDescription = StepTransitionValidator.getStepDescription(nextStep); const stepDescription = StepTransitionValidator.getStepDescription(nextStep);

View File

@@ -1,5 +1,5 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"rootDir": "..", "rootDir": "..",
"outDir": "dist", "outDir": "dist",

View File

@@ -1,5 +1,5 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "dist",
"declaration": true, "declaration": true,

View File

@@ -1,5 +1,5 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"outDir": "dist", "outDir": "dist",

View File

@@ -3,13 +3,14 @@ import type { Season } from '../../domain/entities/Season';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { Game } from '../../domain/entities/Game'; import type { Game } from '../../domain/entities/Game';
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider'; import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
import type { Presenter } from '@gridpilot/shared/presentation';
export interface LeagueSummaryViewModel { export interface LeagueSummaryViewModel {
id: string; id: string;
name: string; name: string;
description: string; description: string;
ownerId: string; ownerId: string;
createdAt: Date; createdAt: string;
maxDrivers: number; maxDrivers: number;
usedDriverSlots: number; usedDriverSlots: number;
maxTeams?: number; maxTeams?: number;
@@ -20,7 +21,7 @@ export interface LeagueSummaryViewModel {
scoring?: { scoring?: {
gameId: string; gameId: string;
gameName: string; gameName: string;
primaryChampionshipType: string; primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
scoringPresetId: string; scoringPresetId: string;
scoringPresetName: string; scoringPresetName: string;
dropPolicySummary: string; dropPolicySummary: string;
@@ -42,6 +43,5 @@ export interface LeagueEnrichedData {
preset?: LeagueScoringPresetDTO; preset?: LeagueScoringPresetDTO;
} }
export interface IAllLeaguesWithCapacityAndScoringPresenter { export interface IAllLeaguesWithCapacityAndScoringPresenter
present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel; extends Presenter<LeagueEnrichedData[], AllLeaguesWithCapacityAndScoringViewModel> {}
}

View File

@@ -1,4 +1,5 @@
import type { League } from '../../domain/entities/League'; import type { League } from '../../domain/entities/League';
import type { Presenter } from '@gridpilot/shared/presentation';
export interface LeagueWithCapacityViewModel { export interface LeagueWithCapacityViewModel {
id: string; id: string;
@@ -24,9 +25,10 @@ export interface AllLeaguesWithCapacityViewModel {
totalCount: number; totalCount: number;
} }
export interface IAllLeaguesWithCapacityPresenter { export interface AllLeaguesWithCapacityResultDTO {
present( leagues: League[];
leagues: League[], memberCounts: Map<string, number>;
memberCounts: Map<string, number>
): AllLeaguesWithCapacityViewModel;
} }
export interface IAllLeaguesWithCapacityPresenter
extends Presenter<AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel> {}

View File

@@ -1,3 +1,5 @@
import type { Presenter } from '@gridpilot/shared/presentation';
export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all'; export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
export interface AllRacesListItemViewModel { export interface AllRacesListItemViewModel {
@@ -21,7 +23,7 @@ export interface AllRacesPageViewModel {
filters: AllRacesFilterOptionsViewModel; filters: AllRacesFilterOptionsViewModel;
} }
export interface IAllRacesPagePresenter { export type AllRacesPageResultDTO = AllRacesPageViewModel;
present(viewModel: AllRacesPageViewModel): void;
getViewModel(): AllRacesPageViewModel | null; export interface IAllRacesPagePresenter
} extends Presenter<AllRacesPageResultDTO, AllRacesPageViewModel> {}

View File

@@ -1,4 +1,3 @@
import type { Team } from '../../domain/entities/Team';
import type { Presenter } from '@gridpilot/shared/presentation'; import type { Presenter } from '@gridpilot/shared/presentation';
export interface TeamListItemViewModel { export interface TeamListItemViewModel {
@@ -19,7 +18,16 @@ export interface AllTeamsViewModel {
} }
export interface AllTeamsResultDTO { export interface AllTeamsResultDTO {
teams: Array<Team & { memberCount: number }>; teams: Array<{
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt: Date;
memberCount: number;
}>;
} }
export interface IAllTeamsPresenter export interface IAllTeamsPresenter

View File

@@ -1,3 +1,5 @@
import type { Presenter } from '@gridpilot/shared/presentation';
export interface DashboardDriverSummaryViewModel { export interface DashboardDriverSummaryViewModel {
id: string; id: string;
name: string; name: string;
@@ -82,7 +84,7 @@ export interface DashboardOverviewViewModel {
friends: DashboardFriendSummaryViewModel[]; friends: DashboardFriendSummaryViewModel[];
} }
export interface IDashboardOverviewPresenter { export type DashboardOverviewResultDTO = DashboardOverviewViewModel;
present(viewModel: DashboardOverviewViewModel): void;
getViewModel(): DashboardOverviewViewModel | null; export interface IDashboardOverviewPresenter
} extends Presenter<DashboardOverviewResultDTO, DashboardOverviewViewModel> {}

View File

@@ -1,5 +1,6 @@
import type { Driver } from '../../domain/entities/Driver'; import type { Driver } from '../../domain/entities/Driver';
import type { SkillLevel } from '../../domain/services/SkillLevelService'; import type { SkillLevel } from '../../domain/services/SkillLevelService';
import type { Presenter } from '@gridpilot/shared/presentation';
export type { SkillLevel }; export type { SkillLevel };
@@ -24,13 +25,21 @@ export interface DriversLeaderboardViewModel {
activeCount: number; activeCount: number;
} }
export interface IDriversLeaderboardPresenter { export interface DriversLeaderboardResultDTO {
present( drivers: Driver[];
drivers: Driver[], rankings: Array<{ driverId: string; rating: number; overallRank: number | null }>;
rankings: Array<{ driverId: string; rating: number; overallRank: number }>, stats: Record<
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>, string,
avatarUrls: Record<string, string> {
): DriversLeaderboardViewModel; rating: number;
wins: number;
getViewModel(): DriversLeaderboardViewModel; podiums: number;
totalRaces: number;
overallRank: number | null;
}
>;
avatarUrls: Record<string, string>;
} }
export interface IDriversLeaderboardPresenter
extends Presenter<DriversLeaderboardResultDTO, DriversLeaderboardViewModel> {}

View File

@@ -1,3 +1,5 @@
import type { Presenter } from '@gridpilot/shared/presentation';
export interface LeagueDriverSeasonStatsItemViewModel { export interface LeagueDriverSeasonStatsItemViewModel {
leagueId: string; leagueId: string;
driverId: string; driverId: string;
@@ -24,18 +26,18 @@ export interface LeagueDriverSeasonStatsViewModel {
stats: LeagueDriverSeasonStatsItemViewModel[]; stats: LeagueDriverSeasonStatsItemViewModel[];
} }
export interface ILeagueDriverSeasonStatsPresenter { export interface LeagueDriverSeasonStatsResultDTO {
present( leagueId: string;
leagueId: string, standings: Array<{
standings: Array<{ driverId: string;
driverId: string; position: number;
position: number; points: number;
points: number; racesCompleted: number;
racesCompleted: number; }>;
}>, penalties: Map<string, { baseDelta: number; bonusDelta: number }>;
penalties: Map<string, { baseDelta: number; bonusDelta: number }>, driverResults: Map<string, Array<{ position: number }>>;
driverResults: Map<string, Array<{ position: number }>>, driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>;
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
): LeagueDriverSeasonStatsViewModel;
getViewModel(): LeagueDriverSeasonStatsViewModel;
} }
export interface ILeagueDriverSeasonStatsPresenter
extends Presenter<LeagueDriverSeasonStatsResultDTO, LeagueDriverSeasonStatsViewModel> {}

View File

@@ -1,5 +1,6 @@
import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig'; import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig';
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider'; import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
import type { Presenter } from '@gridpilot/shared/presentation';
export interface LeagueScoringChampionshipViewModel { export interface LeagueScoringChampionshipViewModel {
id: string; id: string;
@@ -32,7 +33,5 @@ export interface LeagueScoringConfigData {
championships: ChampionshipConfig[]; championships: ChampionshipConfig[];
} }
export interface ILeagueScoringConfigPresenter { export interface ILeagueScoringConfigPresenter
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel; extends Presenter<LeagueScoringConfigData, LeagueScoringConfigViewModel> {}
getViewModel(): LeagueScoringConfigViewModel;
}

View File

@@ -1,4 +1,5 @@
import type { SessionType, RaceStatus } from '../../domain/entities/Race'; import type { SessionType, RaceStatus } from '../../domain/entities/Race';
import type { Presenter } from '@gridpilot/shared/presentation';
export interface RaceDetailEntryViewModel { export interface RaceDetailEntryViewModel {
id: string; id: string;
@@ -55,7 +56,5 @@ export interface RaceDetailViewModel {
error?: string; error?: string;
} }
export interface IRaceDetailPresenter { export interface IRaceDetailPresenter
present(viewModel: RaceDetailViewModel): RaceDetailViewModel; extends Presenter<RaceDetailViewModel, RaceDetailViewModel> {}
getViewModel(): RaceDetailViewModel | null;
}

View File

@@ -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'; import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface RacePenaltyViewModel { export interface RacePenaltyViewModel {
@@ -24,21 +24,7 @@ export interface RacePenaltiesViewModel {
} }
export interface RacePenaltiesResultDTO { export interface RacePenaltiesResultDTO {
penalties: Array<{ penalties: Penalty[];
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;
}>;
driverMap: Map<string, string>; driverMap: Map<string, string>;
} }

View File

@@ -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'; import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface RaceProtestViewModel { export interface RaceProtestViewModel {
@@ -24,20 +24,7 @@ export interface RaceProtestsViewModel {
} }
export interface RaceProtestsResultDTO { export interface RaceProtestsResultDTO {
protests: Array<{ protests: Protest[];
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
incident: ProtestIncident;
comment?: string;
proofVideoUrl?: string;
status: ProtestStatus;
reviewedBy?: string;
decisionNotes?: string;
filedAt: Date;
reviewedAt?: Date;
}>;
driverMap: Map<string, string>; driverMap: Map<string, string>;
} }

View File

@@ -1,9 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation';
export interface RaceRegistrationsViewModel { export interface RaceRegistrationsViewModel {
registeredDriverIds: string[]; registeredDriverIds: string[];
count: number; count: number;
} }
export interface IRaceRegistrationsPresenter { export interface RaceRegistrationsResultDTO {
present(registeredDriverIds: string[]): RaceRegistrationsViewModel; registeredDriverIds: string[];
getViewModel(): RaceRegistrationsViewModel;
} }
export interface IRaceRegistrationsPresenter
extends Presenter<RaceRegistrationsResultDTO, RaceRegistrationsViewModel> {}

View File

@@ -2,6 +2,7 @@ import type { RaceStatus } from '../../domain/entities/Race';
import type { Result } from '../../domain/entities/Result'; import type { Result } from '../../domain/entities/Result';
import type { Driver } from '../../domain/entities/Driver'; import type { Driver } from '../../domain/entities/Driver';
import type { PenaltyType } from '../../domain/entities/Penalty'; import type { PenaltyType } from '../../domain/entities/Penalty';
import type { Presenter } from '@gridpilot/shared/presentation';
export interface RaceResultsHeaderViewModel { export interface RaceResultsHeaderViewModel {
id: string; id: string;
@@ -28,13 +29,11 @@ export interface RaceResultsDetailViewModel {
results: Result[]; results: Result[];
drivers: Driver[]; drivers: Driver[];
penalties: RaceResultsPenaltySummaryViewModel[]; penalties: RaceResultsPenaltySummaryViewModel[];
pointsSystem: Record<number, number>; pointsSystem?: Record<number, number>;
fastestLapTime?: number; fastestLapTime?: number;
currentDriverId?: string; currentDriverId?: string;
error?: string; error?: string;
} }
export interface IRaceResultsDetailPresenter { export interface IRaceResultsDetailPresenter
present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel; extends Presenter<RaceResultsDetailViewModel, RaceResultsDetailViewModel> {}
getViewModel(): RaceResultsDetailViewModel | null;
}

View File

@@ -1,3 +1,5 @@
import type { Presenter } from '@gridpilot/shared/presentation';
export interface RaceWithSOFViewModel { export interface RaceWithSOFViewModel {
id: string; id: string;
leagueId: string; leagueId: string;
@@ -14,21 +16,21 @@ export interface RaceWithSOFViewModel {
participantCount: number; participantCount: number;
} }
export interface IRaceWithSOFPresenter { export interface RaceWithSOFResultDTO {
present( raceId: string;
raceId: string, leagueId: string;
leagueId: string, scheduledAt: Date;
scheduledAt: Date, track: string;
track: string, trackId: string;
trackId: string, car: string;
car: string, carId: string;
carId: string, sessionType: string;
sessionType: string, status: string;
status: string, strengthOfField: number | null;
strengthOfField: number | null, registeredCount: number;
registeredCount: number, maxParticipants: number;
maxParticipants: number, participantCount: number;
participantCount: number
): RaceWithSOFViewModel;
getViewModel(): RaceWithSOFViewModel;
} }
export interface IRaceWithSOFPresenter
extends Presenter<RaceWithSOFResultDTO, RaceWithSOFViewModel> {}

View File

@@ -1,3 +1,5 @@
import type { Presenter } from '@gridpilot/shared/presentation';
export interface RaceListItemViewModel { export interface RaceListItemViewModel {
id: string; id: string;
track: string; track: string;
@@ -25,7 +27,9 @@ export interface RacesPageViewModel {
recentResults: RaceListItemViewModel[]; recentResults: RaceListItemViewModel[];
} }
export interface IRacesPagePresenter { export interface RacesPageResultDTO {
present(races: any[]): void; races: any[];
getViewModel(): RacesPageViewModel;
} }
export interface IRacesPagePresenter
extends Presenter<RacesPageResultDTO, RacesPageViewModel> {}

View File

@@ -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 { export type SponsorDashboardViewModel = SponsorDashboardDTO | null;
present(data: SponsorDashboardDTO | null): void;
} export interface ISponsorDashboardPresenter
extends Presenter<SponsorDashboardDTO | null, SponsorDashboardViewModel> {}

View File

@@ -1,5 +1,7 @@
import type { SponsorSponsorshipsDTO } from '../use-cases/GetSponsorSponsorshipsUseCase'; import type { SponsorSponsorshipsDTO } from '../use-cases/GetSponsorSponsorshipsUseCase';
import type { Presenter } from '@gridpilot/shared/presentation';
export interface ISponsorSponsorshipsPresenter { export type SponsorSponsorshipsViewModel = SponsorSponsorshipsDTO | null;
present(data: SponsorSponsorshipsDTO | null): void;
} export interface ISponsorSponsorshipsPresenter
extends Presenter<SponsorSponsorshipsDTO | null, SponsorSponsorshipsViewModel> {}

View File

@@ -1,5 +1,6 @@
import type { Team } from '../../domain/entities/Team'; import type { Team } from '../../domain/entities/Team';
import type { TeamMembership } from '../../domain/types/TeamMembership'; import type { TeamMembership } from '../../domain/types/TeamMembership';
import type { Presenter } from '@gridpilot/shared/presentation';
export interface TeamDetailsViewModel { export interface TeamDetailsViewModel {
team: { team: {
@@ -9,9 +10,7 @@ export interface TeamDetailsViewModel {
description: string; description: string;
ownerId: string; ownerId: string;
leagues: string[]; leagues: string[];
specialization?: 'endurance' | 'sprint' | 'mixed'; createdAt: string;
region?: string;
languages?: string[];
}; };
membership: { membership: {
role: 'owner' | 'manager' | 'member'; role: 'owner' | 'manager' | 'member';
@@ -21,10 +20,11 @@ export interface TeamDetailsViewModel {
canManage: boolean; canManage: boolean;
} }
export interface ITeamDetailsPresenter { export interface TeamDetailsResultDTO {
present( team: Team;
team: Team, membership: TeamMembership | null;
membership: TeamMembership | null, driverId: string;
driverId: string
): TeamDetailsViewModel;
} }
export interface ITeamDetailsPresenter
extends Presenter<TeamDetailsResultDTO, TeamDetailsViewModel> {}

View File

@@ -27,15 +27,14 @@ export class CreateTeamUseCase {
throw new Error('Driver already belongs to a team'); throw new Error('Driver already belongs to a team');
} }
const team: Team = { const team = Team.create({
id: `team-${Date.now()}`, id: `team-${Date.now()}`,
name, name,
tag, tag,
description, description,
ownerId, ownerId,
leagues, leagues,
createdAt: new Date(), });
};
const createdTeam = await this.teamRepository.create(team); const createdTeam = await this.teamRepository.create(team);

View File

@@ -4,15 +4,25 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider'; import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter'; import type {
import type { AsyncUseCase } from '@gridpilot/shared/application'; 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. * Use Case for retrieving all leagues with capacity and scoring information.
* Orchestrates domain logic and delegates presentation to the presenter. * Orchestrates domain logic and delegates presentation to the presenter.
*/ */
export class GetAllLeaguesWithCapacityAndScoringUseCase export class GetAllLeaguesWithCapacityAndScoringUseCase
implements AsyncUseCase<void, void> implements
UseCase<
void,
LeagueEnrichedData[],
AllLeaguesWithCapacityAndScoringViewModel,
IAllLeaguesWithCapacityAndScoringPresenter
>
{ {
constructor( constructor(
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
@@ -21,10 +31,14 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository, private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider, private readonly presetProvider: LeagueScoringPresetProvider,
public readonly presenter: IAllLeaguesWithCapacityAndScoringPresenter,
) {} ) {}
async execute(): Promise<void> { async execute(
_input: void,
presenter: IAllLeaguesWithCapacityAndScoringPresenter,
): Promise<void> {
presenter.reset();
const leagues = await this.leagueRepository.findAll(); const leagues = await this.leagueRepository.findAll();
const enrichedLeagues: LeagueEnrichedData[] = []; const enrichedLeagues: LeagueEnrichedData[] = [];
@@ -42,18 +56,22 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
).length; ).length;
const seasons = await this.seasonRepository.findByLeagueId(league.id); const seasons = await this.seasonRepository.findByLeagueId(league.id);
const activeSeason = seasons && seasons.length > 0 const activeSeason =
? seasons.find((s) => s.status === 'active') ?? seasons[0] seasons && seasons.length > 0
: undefined; ? seasons.find((s) => s.status === 'active') ?? seasons[0]
: undefined;
let scoringConfig: LeagueEnrichedData['scoringConfig']; let scoringConfig: LeagueEnrichedData['scoringConfig'];
let game: LeagueEnrichedData['game']; let game: LeagueEnrichedData['game'];
let preset: LeagueEnrichedData['preset']; let preset: LeagueEnrichedData['preset'];
if (activeSeason) { if (activeSeason) {
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id); const scoringConfigResult =
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
scoringConfig = scoringConfigResult ?? undefined;
if (scoringConfig) { if (scoringConfig) {
game = await this.gameRepository.findById(activeSeason.gameId); const gameResult = await this.gameRepository.findById(activeSeason.gameId);
game = gameResult ?? undefined;
const presetId = scoringConfig.scoringPresetId; const presetId = scoringConfig.scoringPresetId;
if (presetId) { if (presetId) {
preset = this.presetProvider.getPresetById(presetId); preset = this.presetProvider.getPresetById(presetId);
@@ -64,13 +82,13 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
enrichedLeagues.push({ enrichedLeagues.push({
league, league,
usedDriverSlots, usedDriverSlots,
season: activeSeason, ...(activeSeason ? { season: activeSeason } : {}),
...(scoringConfig ?? undefined ? { scoringConfig } : {}), ...(scoringConfig ? { scoringConfig } : {}),
...(game ?? undefined ? { game } : {}), ...(game ? { game } : {}),
...(preset ?? undefined ? { preset } : {}), ...(preset ? { preset } : {}),
}); });
} }
this.presenter.present(enrichedLeagues); presenter.present(enrichedLeagues);
} }
} }

View File

@@ -1,22 +1,30 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IAllLeaguesWithCapacityPresenter } from '../presenters/IAllLeaguesWithCapacityPresenter'; import type {
import type { AsyncUseCase } from '@gridpilot/shared/application'; IAllLeaguesWithCapacityPresenter,
AllLeaguesWithCapacityResultDTO,
AllLeaguesWithCapacityViewModel,
} from '../presenters/IAllLeaguesWithCapacityPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/** /**
* Use Case for retrieving all leagues with capacity information. * Use Case for retrieving all leagues with capacity information.
* Orchestrates domain logic and delegates presentation to the presenter. * Orchestrates domain logic and delegates presentation to the presenter.
*/ */
export class GetAllLeaguesWithCapacityUseCase export class GetAllLeaguesWithCapacityUseCase
implements AsyncUseCase<void, void> implements UseCase<void, AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel, IAllLeaguesWithCapacityPresenter>
{ {
constructor( constructor(
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
public readonly presenter: IAllLeaguesWithCapacityPresenter,
) {} ) {}
async execute(): Promise<void> { async execute(
_input: void,
presenter: IAllLeaguesWithCapacityPresenter,
): Promise<void> {
presenter.reset();
const leagues = await this.leagueRepository.findAll(); const leagues = await this.leagueRepository.findAll();
const memberCounts = new Map<string, number>(); const memberCounts = new Map<string, number>();
@@ -36,6 +44,11 @@ export class GetAllLeaguesWithCapacityUseCase
memberCounts.set(league.id, usedSlots); memberCounts.set(league.id, usedSlots);
} }
this.presenter.present(leagues, memberCounts); const dto: AllLeaguesWithCapacityResultDTO = {
leagues,
memberCounts,
};
presenter.present(dto);
} }
} }

View File

@@ -2,21 +2,21 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { import type {
IAllRacesPagePresenter, IAllRacesPagePresenter,
AllRacesPageResultDTO,
AllRacesPageViewModel, AllRacesPageViewModel,
AllRacesListItemViewModel, AllRacesListItemViewModel,
AllRacesFilterOptionsViewModel, AllRacesFilterOptionsViewModel,
} from '../presenters/IAllRacesPagePresenter'; } from '../presenters/IAllRacesPagePresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { UseCase } from '@gridpilot/shared/application';
export class GetAllRacesPageDataUseCase export class GetAllRacesPageDataUseCase
implements AsyncUseCase<void, void> { implements UseCase<void, AllRacesPageResultDTO, AllRacesPageViewModel, IAllRacesPagePresenter> {
constructor( constructor(
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
public readonly presenter: IAllRacesPagePresenter,
) {} ) {}
async execute(): Promise<void> { async execute(_input: void, presenter: IAllRacesPagePresenter): Promise<void> {
const [allRaces, allLeagues] = await Promise.all([ const [allRaces, allLeagues] = await Promise.all([
this.raceRepository.findAll(), this.raceRepository.findAll(),
this.leagueRepository.findAll(), this.leagueRepository.findAll(),
@@ -59,6 +59,7 @@ export class GetAllRacesPageDataUseCase
filters, filters,
}; };
this.presenter.present(viewModel); presenter.reset();
presenter.present(viewModel);
} }
} }

View File

@@ -24,11 +24,17 @@ export class GetAllTeamsUseCase
const teams = await this.teamRepository.findAll(); const teams = await this.teamRepository.findAll();
const enrichedTeams: Array<Team & { memberCount: number }> = await Promise.all( const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all(
teams.map(async (team) => { teams.map(async (team) => {
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id); const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
return { return {
...team, id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: [...team.leagues],
createdAt: team.createdAt,
memberCount, memberCount,
}; };
}), }),

View File

@@ -8,7 +8,6 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac
import type { IImageServicePort } from '../ports/IImageServicePort'; import type { IImageServicePort } from '../ports/IImageServicePort';
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { import type {
IDashboardOverviewPresenter, IDashboardOverviewPresenter,
DashboardOverviewViewModel, DashboardOverviewViewModel,
@@ -34,8 +33,7 @@ export interface GetDashboardOverviewParams {
driverId: string; driverId: string;
} }
export class GetDashboardOverviewUseCase export class GetDashboardOverviewUseCase {
implements AsyncUseCase<GetDashboardOverviewParams, void> {
constructor( constructor(
private readonly driverRepository: IDriverRepository, private readonly driverRepository: IDriverRepository,
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
@@ -48,10 +46,9 @@ export class GetDashboardOverviewUseCase
private readonly socialRepository: ISocialGraphRepository, private readonly socialRepository: ISocialGraphRepository,
private readonly imageService: IImageServicePort, private readonly imageService: IImageServicePort,
private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null, private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null,
public readonly presenter: IDashboardOverviewPresenter,
) {} ) {}
async execute(params: GetDashboardOverviewParams): Promise<void> { async execute(params: GetDashboardOverviewParams, presenter: IDashboardOverviewPresenter): Promise<void> {
const { driverId } = params; const { driverId } = params;
const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([ const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([
@@ -137,7 +134,8 @@ export class GetDashboardOverviewUseCase
friends: friendsSummary, friends: friendsSummary,
}; };
this.presenter.present(viewModel); presenter.reset();
presenter.present(viewModel);
} }
private async getDriverLeagues(allLeagues: any[], driverId: string): Promise<any[]> { private async getDriverLeagues(allLeagues: any[], driverId: string): Promise<any[]> {

View File

@@ -2,29 +2,35 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { IRankingService } from '../../domain/services/IRankingService'; import type { IRankingService } from '../../domain/services/IRankingService';
import type { IDriverStatsService } from '../../domain/services/IDriverStatsService'; import type { IDriverStatsService } from '../../domain/services/IDriverStatsService';
import type { IImageServicePort } from '../ports/IImageServicePort'; import type { IImageServicePort } from '../ports/IImageServicePort';
import type { IDriversLeaderboardPresenter } from '../presenters/IDriversLeaderboardPresenter'; import type {
import type { AsyncUseCase } from '@gridpilot/shared/application'; IDriversLeaderboardPresenter,
DriversLeaderboardResultDTO,
DriversLeaderboardViewModel,
} from '../presenters/IDriversLeaderboardPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/** /**
* Use Case for retrieving driver leaderboard data. * Use Case for retrieving driver leaderboard data.
* Orchestrates domain logic and delegates presentation to the presenter. * Orchestrates domain logic and delegates presentation to the presenter.
*/ */
export class GetDriversLeaderboardUseCase export class GetDriversLeaderboardUseCase
implements AsyncUseCase<void, void> { implements UseCase<void, DriversLeaderboardResultDTO, DriversLeaderboardViewModel, IDriversLeaderboardPresenter>
{
constructor( constructor(
private readonly driverRepository: IDriverRepository, private readonly driverRepository: IDriverRepository,
private readonly rankingService: IRankingService, private readonly rankingService: IRankingService,
private readonly driverStatsService: IDriverStatsService, private readonly driverStatsService: IDriverStatsService,
private readonly imageService: IImageServicePort, private readonly imageService: IImageServicePort,
public readonly presenter: IDriversLeaderboardPresenter,
) {} ) {}
async execute(): Promise<void> { async execute(_input: void, presenter: IDriversLeaderboardPresenter): Promise<void> {
presenter.reset();
const drivers = await this.driverRepository.findAll(); const drivers = await this.driverRepository.findAll();
const rankings = this.rankingService.getAllDriverRankings(); const rankings = this.rankingService.getAllDriverRankings();
const stats: Record<string, any> = {}; const stats: DriversLeaderboardResultDTO['stats'] = {};
const avatarUrls: Record<string, string> = {}; const avatarUrls: DriversLeaderboardResultDTO['avatarUrls'] = {};
for (const driver of drivers) { for (const driver of drivers) {
const driverStats = this.driverStatsService.getDriverStats(driver.id); const driverStats = this.driverStatsService.getDriverStats(driver.id);
@@ -34,6 +40,13 @@ export class GetDriversLeaderboardUseCase
avatarUrls[driver.id] = this.imageService.getDriverAvatar(driver.id); 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);
} }
} }

View File

@@ -2,8 +2,12 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep
import type { IResultRepository } from '../../domain/repositories/IResultRepository'; import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter'; import type {
import type { AsyncUseCase } from '@gridpilot/shared/application'; ILeagueDriverSeasonStatsPresenter,
LeagueDriverSeasonStatsResultDTO,
LeagueDriverSeasonStatsViewModel,
} from '../presenters/ILeagueDriverSeasonStatsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface DriverRatingPort { export interface DriverRatingPort {
getRating(driverId: string): { rating: number | null; ratingChange: number | null }; 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. * Orchestrates domain logic and delegates presentation to the presenter.
*/ */
export class GetLeagueDriverSeasonStatsUseCase export class GetLeagueDriverSeasonStatsUseCase
implements AsyncUseCase<GetLeagueDriverSeasonStatsUseCaseParams, void> { implements
UseCase<
GetLeagueDriverSeasonStatsUseCaseParams,
LeagueDriverSeasonStatsResultDTO,
LeagueDriverSeasonStatsViewModel,
ILeagueDriverSeasonStatsPresenter
>
{
constructor( constructor(
private readonly standingRepository: IStandingRepository, private readonly standingRepository: IStandingRepository,
private readonly resultRepository: IResultRepository, private readonly resultRepository: IResultRepository,
private readonly penaltyRepository: IPenaltyRepository, private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly driverRatingPort: DriverRatingPort, private readonly driverRatingPort: DriverRatingPort,
public readonly presenter: ILeagueDriverSeasonStatsPresenter,
) {} ) {}
async execute(params: GetLeagueDriverSeasonStatsUseCaseParams): Promise<void> { async execute(
params: GetLeagueDriverSeasonStatsUseCaseParams,
presenter: ILeagueDriverSeasonStatsPresenter,
): Promise<void> {
presenter.reset();
const { leagueId } = params; const { leagueId } = params;
// Get standings and races for the league // Get standings and races for the league
@@ -70,16 +84,26 @@ export class GetLeagueDriverSeasonStatsUseCase
// Collect driver results // Collect driver results
const driverResults = new Map<string, Array<{ position: number }>>(); const driverResults = new Map<string, Array<{ position: number }>>();
for (const standing of standings) { 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); driverResults.set(standing.driverId, results);
} }
this.presenter.present( const dto: LeagueDriverSeasonStatsResultDTO = {
leagueId, leagueId,
standings, standings: standings.map(standing => ({
penaltiesByDriver, driverId: standing.driverId,
position: standing.position,
points: standing.points,
racesCompleted: standing.racesCompleted,
})),
penalties: penaltiesByDriver,
driverResults, driverResults,
driverRatings driverRatings,
); };
presenter.present(dto);
} }
} }

View File

@@ -49,9 +49,9 @@ export class GetLeagueFullConfigUseCase
const data: LeagueFullConfigData = { const data: LeagueFullConfigData = {
league, league,
activeSeason, ...(activeSeason ? { activeSeason } : {}),
...(scoringConfig ?? undefined ? { scoringConfig } : {}), ...(scoringConfig ? { scoringConfig } : {}),
...(game ?? undefined ? { game } : {}), ...(game ? { game } : {}),
}; };
presenter.reset(); presenter.reset();

View File

@@ -3,25 +3,29 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider'; import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter'; import type {
import type { AsyncUseCase } from '@gridpilot/shared/application'; 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. * Use Case for retrieving a league's scoring configuration for its active season.
* Orchestrates domain logic and delegates presentation to the presenter. * Orchestrates domain logic and delegates presentation to the presenter.
*/ */
export class GetLeagueScoringConfigUseCase export class GetLeagueScoringConfigUseCase
implements AsyncUseCase<{ leagueId: string }, void> { implements UseCase<{ leagueId: string }, LeagueScoringConfigData, LeagueScoringConfigViewModel, ILeagueScoringConfigPresenter>
{
constructor( constructor(
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository, private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository, private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository, private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider, private readonly presetProvider: LeagueScoringPresetProvider,
public readonly presenter: ILeagueScoringConfigPresenter,
) {} ) {}
async execute(params: { leagueId: string }): Promise<void> { async execute(params: { leagueId: string }, presenter: ILeagueScoringConfigPresenter): Promise<void> {
const { leagueId } = params; const { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId); const league = await this.leagueRepository.findById(leagueId);
@@ -65,6 +69,7 @@ export class GetLeagueScoringConfigUseCase
championships: scoringConfig.championships, championships: scoringConfig.championships,
}; };
this.presenter.present(data); presenter.reset();
presenter.present(data);
} }
} }

View File

@@ -14,6 +14,7 @@ import type {
RaceDetailEntryViewModel, RaceDetailEntryViewModel,
RaceDetailUserResultViewModel, RaceDetailUserResultViewModel,
} from '../presenters/IRaceDetailPresenter'; } from '../presenters/IRaceDetailPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/** /**
* Use Case: GetRaceDetailUseCase * Use Case: GetRaceDetailUseCase
@@ -30,7 +31,9 @@ export interface GetRaceDetailQueryParams {
driverId: string; driverId: string;
} }
export class GetRaceDetailUseCase { export class GetRaceDetailUseCase
implements UseCase<GetRaceDetailQueryParams, RaceDetailViewModel, RaceDetailViewModel, IRaceDetailPresenter>
{
constructor( constructor(
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
@@ -40,10 +43,11 @@ export class GetRaceDetailUseCase {
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRatingProvider: DriverRatingProvider, private readonly driverRatingProvider: DriverRatingProvider,
private readonly imageService: IImageServicePort, private readonly imageService: IImageServicePort,
public readonly presenter: IRaceDetailPresenter,
) {} ) {}
async execute(params: GetRaceDetailQueryParams): Promise<void> { async execute(params: GetRaceDetailQueryParams, presenter: IRaceDetailPresenter): Promise<void> {
presenter.reset();
const { raceId, driverId } = params; const { raceId, driverId } = params;
const race = await this.raceRepository.findById(raceId); const race = await this.raceRepository.findById(raceId);
@@ -59,7 +63,7 @@ export class GetRaceDetailUseCase {
userResult: null, userResult: null,
error: 'Race not found', error: 'Race not found',
}; };
this.presenter.present(emptyViewModel); presenter.present(emptyViewModel);
return; return;
} }
@@ -121,8 +125,8 @@ export class GetRaceDetailUseCase {
sessionType: race.sessionType, sessionType: race.sessionType,
status: race.status, status: race.status,
strengthOfField: race.strengthOfField ?? null, strengthOfField: race.strengthOfField ?? null,
registeredCount: race.registeredCount, ...(race.registeredCount !== undefined ? { registeredCount: race.registeredCount } : {}),
maxParticipants: race.maxParticipants, ...(race.maxParticipants !== undefined ? { maxParticipants: race.maxParticipants } : {}),
}; };
const leagueView: RaceDetailLeagueViewModel | null = league const leagueView: RaceDetailLeagueViewModel | null = league
@@ -131,8 +135,12 @@ export class GetRaceDetailUseCase {
name: league.name, name: league.name,
description: league.description, description: league.description,
settings: { settings: {
maxDrivers: league.settings.maxDrivers, ...(league.settings.maxDrivers !== undefined
qualifyingFormat: league.settings.qualifyingFormat, ? { maxDrivers: league.settings.maxDrivers }
: {}),
...(league.settings.qualifyingFormat !== undefined
? { qualifyingFormat: league.settings.qualifyingFormat }
: {}),
}, },
} }
: null; : null;
@@ -148,7 +156,7 @@ export class GetRaceDetailUseCase {
userResult: userResultView, userResult: userResultView,
}; };
this.presenter.present(viewModel); presenter.present(viewModel);
} }
private calculateRatingChange(position: number): number { private calculateRatingChange(position: number): number {

View File

@@ -1,6 +1,11 @@
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository'; import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO'; 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 * Use Case: GetRaceRegistrationsUseCase
@@ -8,15 +13,26 @@ import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistratio
* Returns registered driver IDs for a race. * Returns registered driver IDs for a race.
* Orchestrates domain logic and delegates presentation to the presenter. * Orchestrates domain logic and delegates presentation to the presenter.
*/ */
export class GetRaceRegistrationsUseCase { export class GetRaceRegistrationsUseCase
implements UseCase<GetRaceRegistrationsQueryParamsDTO, RaceRegistrationsResultDTO, RaceRegistrationsViewModel, IRaceRegistrationsPresenter>
{
constructor( constructor(
private readonly registrationRepository: IRaceRegistrationRepository, private readonly registrationRepository: IRaceRegistrationRepository,
public readonly presenter: IRaceRegistrationsPresenter,
) {} ) {}
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<void> { async execute(
params: GetRaceRegistrationsQueryParamsDTO,
presenter: IRaceRegistrationsPresenter,
): Promise<void> {
presenter.reset();
const { raceId } = params; const { raceId } = params;
const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId); const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId);
this.presenter.present(registeredDriverIds);
const dto: RaceRegistrationsResultDTO = {
registeredDriverIds,
};
presenter.present(dto);
} }
} }

View File

@@ -8,6 +8,7 @@ import type {
RaceResultsDetailViewModel, RaceResultsDetailViewModel,
RaceResultsPenaltySummaryViewModel, RaceResultsPenaltySummaryViewModel,
} from '../presenters/IRaceResultsDetailPresenter'; } from '../presenters/IRaceResultsDetailPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
import type { League } from '../../domain/entities/League'; import type { League } from '../../domain/entities/League';
import type { Result } from '../../domain/entities/Result'; import type { Result } from '../../domain/entities/Result';
import type { Driver } from '../../domain/entities/Driver'; import type { Driver } from '../../domain/entities/Driver';
@@ -18,8 +19,8 @@ export interface GetRaceResultsDetailParams {
driverId?: string; driverId?: string;
} }
function buildPointsSystem(league: League | null): Record<number, number> { function buildPointsSystem(league: League | null): Record<number, number> | undefined {
if (!league) return {}; if (!league) return undefined;
const pointsSystems: Record<string, Record<number, number>> = { const pointsSystems: Record<string, Record<number, number>> = {
'f1-2024': { 'f1-2024': {
@@ -53,11 +54,17 @@ function buildPointsSystem(league: League | null): Record<number, number> {
}, },
}; };
return ( const customPoints = league.settings.customPoints;
league.settings.customPoints || if (customPoints) {
pointsSystems[league.settings.pointsSystem] || return customPoints;
pointsSystems['f1-2024'] }
);
const preset = pointsSystems[league.settings.pointsSystem];
if (preset) {
return preset;
}
return pointsSystems['f1-2024'];
} }
function getFastestLapTime(results: Result[]): number | undefined { 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( constructor(
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly resultRepository: IResultRepository, private readonly resultRepository: IResultRepository,
private readonly driverRepository: IDriverRepository, private readonly driverRepository: IDriverRepository,
private readonly penaltyRepository: IPenaltyRepository, private readonly penaltyRepository: IPenaltyRepository,
public readonly presenter: IRaceResultsDetailPresenter,
) {} ) {}
async execute(params: GetRaceResultsDetailParams): Promise<void> { async execute(
params: GetRaceResultsDetailParams,
presenter: IRaceResultsDetailPresenter,
): Promise<void> {
presenter.reset();
const { raceId, driverId } = params; const { raceId, driverId } = params;
const race = await this.raceRepository.findById(raceId); const race = await this.raceRepository.findById(raceId);
@@ -95,11 +113,10 @@ export class GetRaceResultsDetailUseCase {
results: [], results: [],
drivers: [], drivers: [],
penalties: [], penalties: [],
pointsSystem: {}, ...(driverId ? { currentDriverId: driverId } : {}),
currentDriverId: driverId,
error: 'Race not found', error: 'Race not found',
}; };
this.presenter.present(errorViewModel); presenter.present(errorViewModel);
return; return;
} }
@@ -111,7 +128,7 @@ export class GetRaceResultsDetailUseCase {
]); ]);
const effectiveCurrentDriverId = 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 pointsSystem = buildPointsSystem(league as League | null);
const fastestLapTime = getFastestLapTime(results); const fastestLapTime = getFastestLapTime(results);
@@ -134,11 +151,11 @@ export class GetRaceResultsDetailUseCase {
results, results,
drivers, drivers,
penalties: penaltySummary, penalties: penaltySummary,
pointsSystem, ...(pointsSystem ? { pointsSystem } : {}),
...(fastestLapTime !== undefined ? { fastestLapTime } : {}), ...(fastestLapTime !== undefined ? { fastestLapTime } : {}),
currentDriverId: effectiveCurrentDriverId, ...(effectiveCurrentDriverId ? { currentDriverId: effectiveCurrentDriverId } : {}),
}; };
this.presenter.present(viewModel); presenter.present(viewModel);
} }
} }

View File

@@ -14,13 +14,16 @@ import {
AverageStrengthOfFieldCalculator, AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator, type StrengthOfFieldCalculator,
} from '../../domain/services/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 { export interface GetRaceWithSOFQueryParams {
raceId: string; raceId: string;
} }
export class GetRaceWithSOFUseCase { export class GetRaceWithSOFUseCase
implements UseCase<GetRaceWithSOFQueryParams, RaceWithSOFResultDTO, import('../presenters/IRaceWithSOFPresenter').RaceWithSOFViewModel, IRaceWithSOFPresenter>
{
private readonly sofCalculator: StrengthOfFieldCalculator; private readonly sofCalculator: StrengthOfFieldCalculator;
constructor( constructor(
@@ -28,18 +31,19 @@ export class GetRaceWithSOFUseCase {
private readonly registrationRepository: IRaceRegistrationRepository, private readonly registrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository, private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider, private readonly driverRatingProvider: DriverRatingProvider,
public readonly presenter: IRaceWithSOFPresenter,
sofCalculator?: StrengthOfFieldCalculator, sofCalculator?: StrengthOfFieldCalculator,
) { ) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator(); this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
} }
async execute(params: GetRaceWithSOFQueryParams): Promise<void> { async execute(params: GetRaceWithSOFQueryParams, presenter: IRaceWithSOFPresenter): Promise<void> {
presenter.reset();
const { raceId } = params; const { raceId } = params;
const race = await this.raceRepository.findById(raceId); const race = await this.raceRepository.findById(raceId);
if (!race) { if (!race) {
return null; return;
} }
// Get participant IDs based on race status // Get participant IDs based on race status
@@ -66,20 +70,24 @@ export class GetRaceWithSOFUseCase {
strengthOfField = this.sofCalculator.calculate(driverRatings); strengthOfField = this.sofCalculator.calculate(driverRatings);
} }
this.presenter.present( presenter.reset();
race.id,
race.leagueId, const dto: RaceWithSOFResultDTO = {
race.scheduledAt, raceId: race.id,
race.track, leagueId: race.leagueId,
race.trackId, scheduledAt: race.scheduledAt,
race.car, track: race.track ?? '',
race.carId, trackId: race.trackId ?? '',
race.sessionType, car: race.car ?? '',
race.status, carId: race.carId ?? '',
sessionType: race.sessionType,
status: race.status,
strengthOfField, strengthOfField,
race.registeredCount ?? participantIds.length, registeredCount: race.registeredCount ?? participantIds.length,
race.maxParticipants, maxParticipants: race.maxParticipants ?? participantIds.length,
participantIds.length participantCount: participantIds.length,
); };
presenter.present(dto);
} }
} }

View File

@@ -1,15 +1,23 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; 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<void, RacesPageResultDTO, RacesPageViewModel, IRacesPagePresenter>
{
constructor( constructor(
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
public readonly presenter: IRacesPagePresenter,
) {} ) {}
async execute(): Promise<void> { async execute(_input: void, presenter: IRacesPagePresenter): Promise<void> {
presenter.reset();
const [allRaces, allLeagues] = await Promise.all([ const [allRaces, allLeagues] = await Promise.all([
this.raceRepository.findAll(), this.raceRepository.findAll(),
this.leagueRepository.findAll(), this.leagueRepository.findAll(),
@@ -33,6 +41,10 @@ export class GetRacesPageDataUseCase {
isPast: race.isPast(), isPast: race.isPast(),
})); }));
this.presenter.present(races); const dto: RacesPageResultDTO = {
races,
};
presenter.present(dto);
} }
} }

View File

@@ -10,7 +10,11 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; 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 { export interface GetSponsorDashboardQueryParams {
sponsorId: string; sponsorId: string;
@@ -47,7 +51,9 @@ export interface SponsorDashboardDTO {
}; };
} }
export class GetSponsorDashboardUseCase { export class GetSponsorDashboardUseCase
implements UseCase<GetSponsorDashboardQueryParams, SponsorDashboardDTO | null, SponsorDashboardViewModel, ISponsorDashboardPresenter>
{
constructor( constructor(
private readonly sponsorRepository: ISponsorRepository, private readonly sponsorRepository: ISponsorRepository,
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository, private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
@@ -55,15 +61,19 @@ export class GetSponsorDashboardUseCase {
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly presenter: ISponsorDashboardPresenter,
) {} ) {}
async execute(params: GetSponsorDashboardQueryParams): Promise<void> { async execute(
params: GetSponsorDashboardQueryParams,
presenter: ISponsorDashboardPresenter,
): Promise<void> {
presenter.reset();
const { sponsorId } = params; const { sponsorId } = params;
const sponsor = await this.sponsorRepository.findById(sponsorId); const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) { if (!sponsor) {
this.presenter.present(null); presenter.present(null);
return; return;
} }
@@ -143,7 +153,7 @@ export class GetSponsorDashboardUseCase {
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10)) ? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
: 0; : 0;
this.presenter.present({ const dto: SponsorDashboardDTO = {
sponsorId, sponsorId,
sponsorName: sponsor.name, sponsorName: sponsor.name,
metrics: { metrics: {
@@ -162,6 +172,8 @@ export class GetSponsorDashboardUseCase {
totalInvestment, totalInvestment,
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100, costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
}, },
}); };
presenter.present(dto);
} }
} }

View File

@@ -11,7 +11,11 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship'; 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 { export interface GetSponsorSponsorshipsQueryParams {
sponsorId: string; sponsorId: string;
@@ -62,7 +66,9 @@ export interface SponsorSponsorshipsDTO {
}; };
} }
export class GetSponsorSponsorshipsUseCase { export class GetSponsorSponsorshipsUseCase
implements UseCase<GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO | null, SponsorSponsorshipsViewModel, ISponsorSponsorshipsPresenter>
{
constructor( constructor(
private readonly sponsorRepository: ISponsorRepository, private readonly sponsorRepository: ISponsorRepository,
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository, private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
@@ -70,15 +76,19 @@ export class GetSponsorSponsorshipsUseCase {
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly presenter: ISponsorSponsorshipsPresenter,
) {} ) {}
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<void> { async execute(
params: GetSponsorSponsorshipsQueryParams,
presenter: ISponsorSponsorshipsPresenter,
): Promise<void> {
presenter.reset();
const { sponsorId } = params; const { sponsorId } = params;
const sponsor = await this.sponsorRepository.findById(sponsorId); const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) { if (!sponsor) {
this.presenter.present(null); presenter.present(null);
return; return;
} }
@@ -150,7 +160,7 @@ export class GetSponsorSponsorshipsUseCase {
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length; const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
this.presenter.present({ const dto: SponsorSponsorshipsDTO = {
sponsorId, sponsorId,
sponsorName: sponsor.name, sponsorName: sponsor.name,
sponsorships: sponsorshipDetails, sponsorships: sponsorshipDetails,
@@ -161,6 +171,8 @@ export class GetSponsorSponsorshipsUseCase {
totalPlatformFees, totalPlatformFees,
currency: 'USD', currency: 'USD',
}, },
}); };
presenter.present(dto);
} }
} }

View File

@@ -1,19 +1,31 @@
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; 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. * Use Case for retrieving team details.
* Orchestrates domain logic and delegates presentation to the presenter. * 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( constructor(
private readonly teamRepository: ITeamRepository, private readonly teamRepository: ITeamRepository,
private readonly membershipRepository: ITeamMembershipRepository, private readonly membershipRepository: ITeamMembershipRepository,
public readonly presenter: ITeamDetailsPresenter,
) {} ) {}
async execute(teamId: string, driverId: string): Promise<void> { async execute(
params: { teamId: string; driverId: string },
presenter: ITeamDetailsPresenter,
): Promise<void> {
presenter.reset();
const { teamId, driverId } = params;
const team = await this.teamRepository.findById(teamId); const team = await this.teamRepository.findById(teamId);
if (!team) { if (!team) {
throw new Error('Team not found'); throw new Error('Team not found');
@@ -21,6 +33,12 @@ export class GetTeamDetailsUseCase {
const membership = await this.membershipRepository.getMembership(teamId, driverId); const membership = await this.membershipRepository.getMembership(teamId, driverId);
this.presenter.present(team, membership, driverId); const dto: TeamDetailsResultDTO = {
team,
membership,
driverId,
};
presenter.present(dto);
} }
} }

View File

@@ -17,6 +17,10 @@ export class PreviewLeagueScheduleUseCase {
execute(params: PreviewLeagueScheduleQueryParams): void { execute(params: PreviewLeagueScheduleQueryParams): void {
const seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule); const seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule);
if (!seasonSchedule) {
throw new Error('Invalid schedule data');
}
const maxRounds = const maxRounds =
params.maxRounds && params.maxRounds > 0 params.maxRounds && params.maxRounds > 0
? Math.min(params.maxRounds, seasonSchedule.plannedRounds) ? Math.min(params.maxRounds, seasonSchedule.plannedRounds)
@@ -46,8 +50,11 @@ export class PreviewLeagueScheduleUseCase {
return 'No rounds scheduled.'; return 'No rounds scheduled.';
} }
const first = new Date(rounds[0].scheduledAt); const firstRound = rounds[0]!;
const last = new Date(rounds[rounds.length - 1].scheduledAt); 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 firstDate = first.toISOString().slice(0, 10);
const lastDate = last.toISOString().slice(0, 10); const lastDate = last.toISOString().slice(0, 10);

View File

@@ -22,10 +22,12 @@ export class UpdateTeamUseCase {
throw new Error('Team not found'); throw new Error('Team not found');
} }
const updated: Team = { const updated = existing.update({
...existing, ...(updates.name !== undefined && { name: updates.name }),
...updates, ...(updates.tag !== undefined && { tag: updates.tag }),
}; ...(updates.description !== undefined && { description: updates.description }),
...(updates.leagues !== undefined && { leagues: updates.leagues }),
});
await this.teamRepository.update(updated); await this.teamRepository.update(updated);
} }

View File

@@ -19,9 +19,9 @@ export class Car implements IEntity<string> {
readonly carClass: CarClass; readonly carClass: CarClass;
readonly license: CarLicense; readonly license: CarLicense;
readonly year: number; readonly year: number;
readonly horsepower?: number; readonly horsepower: number | undefined;
readonly weight?: number; readonly weight: number | undefined;
readonly imageUrl?: string; readonly imageUrl: string | undefined;
readonly gameId: string; readonly gameId: string;
private constructor(props: { private constructor(props: {

View File

@@ -13,7 +13,7 @@ export class Driver implements IEntity<string> {
readonly iracingId: string; readonly iracingId: string;
readonly name: string; readonly name: string;
readonly country: string; readonly country: string;
readonly bio?: string; readonly bio: string | undefined;
readonly joinedAt: Date; readonly joinedAt: Date;
private constructor(props: { private constructor(props: {
@@ -92,14 +92,18 @@ export class Driver implements IEntity<string> {
update(props: Partial<{ update(props: Partial<{
name: string; name: string;
country: string; country: string;
bio: string; bio?: string;
}>): Driver { }>): Driver {
const nextName = props.name ?? this.name;
const nextCountry = props.country ?? this.country;
const nextBio = props.bio ?? this.bio;
return new Driver({ return new Driver({
id: this.id, id: this.id,
iracingId: this.iracingId, iracingId: this.iracingId,
name: props.name ?? this.name, name: nextName,
country: props.country ?? this.country, country: nextCountry,
bio: props.bio ?? this.bio, ...(nextBio !== undefined ? { bio: nextBio } : {}),
joinedAt: this.joinedAt, joinedAt: this.joinedAt,
}); });
} }

View File

@@ -87,7 +87,7 @@ export class League implements IEntity<string> {
readonly ownerId: string; readonly ownerId: string;
readonly settings: LeagueSettings; readonly settings: LeagueSettings;
readonly createdAt: Date; readonly createdAt: Date;
readonly socialLinks?: LeagueSocialLinks; readonly socialLinks: LeagueSocialLinks | undefined;
private constructor(props: { private constructor(props: {
id: string; id: string;
@@ -140,6 +140,8 @@ export class League implements IEntity<string> {
stewarding: defaultStewardingSettings, stewarding: defaultStewardingSettings,
}; };
const socialLinks = props.socialLinks;
return new League({ return new League({
id: props.id, id: props.id,
name: props.name, name: props.name,
@@ -147,7 +149,7 @@ export class League implements IEntity<string> {
ownerId: props.ownerId, ownerId: props.ownerId,
settings: { ...defaultSettings, ...props.settings }, settings: { ...defaultSettings, ...props.settings },
createdAt: props.createdAt ?? new Date(), createdAt: props.createdAt ?? new Date(),
socialLinks: props.socialLinks, ...(socialLinks !== undefined ? { socialLinks } : {}),
}); });
} }
@@ -189,7 +191,7 @@ export class League implements IEntity<string> {
description: string; description: string;
ownerId: string; ownerId: string;
settings: LeagueSettings; settings: LeagueSettings;
socialLinks: LeagueSocialLinks | undefined; socialLinks?: LeagueSocialLinks;
}>): League { }>): League {
return new League({ return new League({
id: this.id, id: this.id,
@@ -198,7 +200,11 @@ export class League implements IEntity<string> {
ownerId: props.ownerId ?? this.ownerId, ownerId: props.ownerId ?? this.ownerId,
settings: props.settings ?? this.settings, settings: props.settings ?? this.settings,
createdAt: this.createdAt, createdAt: this.createdAt,
socialLinks: props.socialLinks ?? this.socialLinks, ...(props.socialLinks !== undefined
? { socialLinks: props.socialLinks }
: this.socialLinks !== undefined
? { socialLinks: this.socialLinks }
: {}),
}); });
} }
} }

View File

@@ -102,12 +102,16 @@ export class Penalty implements IEntity<string> {
if (this.props.status === 'overturned') { if (this.props.status === 'overturned') {
throw new RacingDomainInvariantError('Cannot apply an overturned penalty'); throw new RacingDomainInvariantError('Cannot apply an overturned penalty');
} }
return new Penalty({ const base: PenaltyProps = {
...this.props, ...this.props,
status: 'applied', status: 'applied',
appliedAt: new Date(), appliedAt: new Date(),
notes, };
});
const next: PenaltyProps =
notes !== undefined ? { ...base, notes } : base;
return Penalty.create(next);
} }
/** /**

View File

@@ -153,14 +153,18 @@ export class Protest implements IEntity<string> {
if (!statement?.trim()) { if (!statement?.trim()) {
throw new RacingDomainValidationError('Defense statement is required'); 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({ return new Protest({
...this.props, ...this.props,
status: 'under_review', status: 'under_review',
defense: { defense: nextDefense,
statement: statement.trim(),
videoUrl,
submittedAt: new Date(),
},
}); });
} }

View File

@@ -16,14 +16,14 @@ export class Race implements IEntity<string> {
readonly leagueId: string; readonly leagueId: string;
readonly scheduledAt: Date; readonly scheduledAt: Date;
readonly track: string; readonly track: string;
readonly trackId?: string; readonly trackId: string | undefined;
readonly car: string; readonly car: string;
readonly carId?: string; readonly carId: string | undefined;
readonly sessionType: SessionType; readonly sessionType: SessionType;
readonly status: RaceStatus; readonly status: RaceStatus;
readonly strengthOfField?: number; readonly strengthOfField: number | undefined;
readonly registeredCount?: number; readonly registeredCount: number | undefined;
readonly maxParticipants?: number; readonly maxParticipants: number | undefined;
private constructor(props: { private constructor(props: {
id: string; id: string;
@@ -127,10 +127,34 @@ export class Race implements IEntity<string> {
throw new RacingDomainInvariantError('Only scheduled races can be started'); throw new RacingDomainInvariantError('Only scheduled races can be started');
} }
return new Race({ const base = {
...this, id: this.id,
status: 'running', 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<string> {
throw new RacingDomainInvariantError('Cannot complete a cancelled race'); throw new RacingDomainInvariantError('Cannot complete a cancelled race');
} }
return new Race({ const base = {
...this, id: this.id,
status: 'completed', 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<string> {
throw new RacingDomainInvariantError('Race is already cancelled'); throw new RacingDomainInvariantError('Race is already cancelled');
} }
return new Race({ const base = {
...this, id: this.id,
status: 'cancelled', 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 * Update SOF and participant count
*/ */
updateField(strengthOfField: number, registeredCount: number): Race { updateField(strengthOfField: number, registeredCount: number): Race {
return new Race({ const base = {
...this, id: this.id,
leagueId: this.leagueId,
scheduledAt: this.scheduledAt,
track: this.track,
car: this.car,
sessionType: this.sessionType,
status: this.status,
strengthOfField, strengthOfField,
registeredCount, 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);
} }
/** /**

View File

@@ -8,11 +8,11 @@ export class Season implements IEntity<string> {
readonly leagueId: string; readonly leagueId: string;
readonly gameId: string; readonly gameId: string;
readonly name: string; readonly name: string;
readonly year?: number; readonly year: number | undefined;
readonly order?: number; readonly order: number | undefined;
readonly status: SeasonStatus; readonly status: SeasonStatus;
readonly startDate?: Date; readonly startDate: Date | undefined;
readonly endDate?: Date; readonly endDate: Date | undefined;
private constructor(props: { private constructor(props: {
id: string; id: string;

View File

@@ -33,8 +33,8 @@ export class SeasonSponsorship implements IEntity<string> {
readonly pricing: Money; readonly pricing: Money;
readonly status: SponsorshipStatus; readonly status: SponsorshipStatus;
readonly createdAt: Date; readonly createdAt: Date;
readonly activatedAt?: Date; readonly activatedAt: Date | undefined;
readonly description?: string; readonly description: string | undefined;
private constructor(props: SeasonSponsorshipProps) { private constructor(props: SeasonSponsorshipProps) {
this.id = props.id; this.id = props.id;
@@ -105,11 +105,23 @@ export class SeasonSponsorship implements IEntity<string> {
throw new RacingDomainInvariantError('Cannot activate a cancelled SeasonSponsorship'); throw new RacingDomainInvariantError('Cannot activate a cancelled SeasonSponsorship');
} }
return new SeasonSponsorship({ const base: SeasonSponsorshipProps = {
...this, id: this.id,
seasonId: this.seasonId,
sponsorId: this.sponsorId,
tier: this.tier,
pricing: this.pricing,
status: 'active', status: 'active',
createdAt: this.createdAt,
activatedAt: new Date(), 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<string> {
throw new RacingDomainInvariantError('SeasonSponsorship is already cancelled'); throw new RacingDomainInvariantError('SeasonSponsorship is already cancelled');
} }
return new SeasonSponsorship({ const base: SeasonSponsorshipProps = {
...this, id: this.id,
seasonId: this.seasonId,
sponsorId: this.sponsorId,
tier: this.tier,
pricing: this.pricing,
status: 'cancelled', 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);
} }
/** /**

View File

@@ -20,8 +20,8 @@ export class Sponsor implements IEntity<string> {
readonly id: string; readonly id: string;
readonly name: string; readonly name: string;
readonly contactEmail: string; readonly contactEmail: string;
readonly logoUrl?: string; readonly logoUrl: string | undefined;
readonly websiteUrl?: string; readonly websiteUrl: string | undefined;
readonly createdAt: Date; readonly createdAt: Date;
private constructor(props: SponsorProps) { private constructor(props: SponsorProps) {
@@ -36,10 +36,22 @@ export class Sponsor implements IEntity<string> {
static create(props: Omit<SponsorProps, 'createdAt'> & { createdAt?: Date }): Sponsor { static create(props: Omit<SponsorProps, 'createdAt'> & { createdAt?: Date }): Sponsor {
this.validate(props); this.validate(props);
return new Sponsor({ const { createdAt, ...rest } = props;
...props, const base = {
createdAt: props.createdAt ?? new Date(), 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<SponsorProps, 'createdAt'>): void { private static validate(props: Omit<SponsorProps, 'createdAt'>): void {
@@ -80,18 +92,30 @@ export class Sponsor implements IEntity<string> {
update(props: Partial<{ update(props: Partial<{
name: string; name: string;
contactEmail: string; contactEmail: string;
logoUrl: string | undefined; logoUrl?: string;
websiteUrl: string | undefined; websiteUrl?: string;
}>): Sponsor { }>): Sponsor {
const updated = { const updatedBase = {
id: this.id, id: this.id,
name: props.name ?? this.name, name: props.name ?? this.name,
contactEmail: props.contactEmail ?? this.contactEmail, contactEmail: props.contactEmail ?? this.contactEmail,
logoUrl: props.logoUrl !== undefined ? props.logoUrl : this.logoUrl,
websiteUrl: props.websiteUrl !== undefined ? props.websiteUrl : this.websiteUrl,
createdAt: this.createdAt, 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); Sponsor.validate(updated);
return new Sponsor(updated); return new Sponsor(updated);
} }

View File

@@ -36,12 +36,12 @@ export class SponsorshipRequest implements IEntity<string> {
readonly entityId: string; readonly entityId: string;
readonly tier: SponsorshipTier; readonly tier: SponsorshipTier;
readonly offeredAmount: Money; readonly offeredAmount: Money;
readonly message?: string; readonly message: string | undefined;
readonly status: SponsorshipRequestStatus; readonly status: SponsorshipRequestStatus;
readonly createdAt: Date; readonly createdAt: Date;
readonly respondedAt?: Date; readonly respondedAt: Date | undefined;
readonly respondedBy?: string; readonly respondedBy: string | undefined;
readonly rejectionReason?: string; readonly rejectionReason: string | undefined;
private constructor(props: SponsorshipRequestProps) { private constructor(props: SponsorshipRequestProps) {
this.id = props.id; this.id = props.id;
@@ -113,12 +113,28 @@ export class SponsorshipRequest implements IEntity<string> {
throw new RacingDomainValidationError('respondedBy is required when accepting'); throw new RacingDomainValidationError('respondedBy is required when accepting');
} }
return new SponsorshipRequest({ const base: SponsorshipRequestProps = {
...this, id: this.id,
sponsorId: this.sponsorId,
entityType: this.entityType,
entityId: this.entityId,
tier: this.tier,
offeredAmount: this.offeredAmount,
status: 'accepted', status: 'accepted',
createdAt: this.createdAt,
respondedAt: new Date(), respondedAt: new Date(),
respondedBy, 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<string> {
throw new RacingDomainValidationError('respondedBy is required when rejecting'); throw new RacingDomainValidationError('respondedBy is required when rejecting');
} }
return new SponsorshipRequest({ const base: SponsorshipRequestProps = {
...this, id: this.id,
sponsorId: this.sponsorId,
entityType: this.entityType,
entityId: this.entityId,
tier: this.tier,
offeredAmount: this.offeredAmount,
status: 'rejected', status: 'rejected',
createdAt: this.createdAt,
respondedAt: new Date(), respondedAt: new Date(),
respondedBy, 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<string> {
throw new RacingDomainInvariantError(`Cannot withdraw a ${this.status} sponsorship request`); throw new RacingDomainInvariantError(`Cannot withdraw a ${this.status} sponsorship request`);
} }
return new SponsorshipRequest({ const base: SponsorshipRequestProps = {
...this, id: this.id,
sponsorId: this.sponsorId,
entityType: this.entityType,
entityId: this.entityId,
tier: this.tier,
offeredAmount: this.offeredAmount,
status: 'withdrawn', status: 'withdrawn',
createdAt: this.createdAt,
respondedAt: new Date(), 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);
} }
/** /**

View File

@@ -5,7 +5,7 @@
* Immutable entity with factory methods and domain validation. * 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'; import type { IEntity } from '@gridpilot/shared/domain';
export class Standing implements IEntity<string> { export class Standing implements IEntity<string> {
@@ -104,12 +104,17 @@ export class Standing implements IEntity<string> {
*/ */
updatePosition(position: number): Standing { updatePosition(position: number): Standing {
if (!Number.isInteger(position) || position < 1) { 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({ return Standing.create({
...this, id: this.id,
leagueId: this.leagueId,
driverId: this.driverId,
points: this.points,
wins: this.wins,
position, position,
racesCompleted: this.racesCompleted,
}); });
} }

View File

@@ -20,7 +20,7 @@ export class Track implements IEntity<string> {
readonly difficulty: TrackDifficulty; readonly difficulty: TrackDifficulty;
readonly lengthKm: number; readonly lengthKm: number;
readonly turns: number; readonly turns: number;
readonly imageUrl?: string; readonly imageUrl: string | undefined;
readonly gameId: string; readonly gameId: string;
private constructor(props: { private constructor(props: {
@@ -32,7 +32,7 @@ export class Track implements IEntity<string> {
difficulty: TrackDifficulty; difficulty: TrackDifficulty;
lengthKm: number; lengthKm: number;
turns: number; turns: number;
imageUrl?: string; imageUrl?: string | undefined;
gameId: string; gameId: string;
}) { }) {
this.id = props.id; this.id = props.id;
@@ -64,7 +64,7 @@ export class Track implements IEntity<string> {
}): Track { }): Track {
this.validate(props); this.validate(props);
return new Track({ const base = {
id: props.id, id: props.id,
name: props.name, name: props.name,
shortName: props.shortName ?? props.name.slice(0, 3).toUpperCase(), shortName: props.shortName ?? props.name.slice(0, 3).toUpperCase(),
@@ -73,9 +73,13 @@ export class Track implements IEntity<string> {
difficulty: props.difficulty ?? 'intermediate', difficulty: props.difficulty ?? 'intermediate',
lengthKm: props.lengthKm, lengthKm: props.lengthKm,
turns: props.turns, turns: props.turns,
imageUrl: props.imageUrl,
gameId: props.gameId, gameId: props.gameId,
}); };
const withImage =
props.imageUrl !== undefined ? { ...base, imageUrl: props.imageUrl } : base;
return new Track(withImage);
} }
/** /**

View File

@@ -36,6 +36,11 @@ export interface ITeamMembershipRepository {
*/ */
removeMembership(teamId: string, driverId: string): Promise<void>; removeMembership(teamId: string, driverId: string): Promise<void>;
/**
* Count active members for a team.
*/
countByTeamId(teamId: string): Promise<number>;
/** /**
* Get all join requests for a team. * Get all join requests for a team.
*/ */

Some files were not shown because too many files have changed in this diff Show More