wip
This commit is contained in:
@@ -255,7 +255,18 @@ export function configureDIContainer(): void {
|
||||
}
|
||||
|
||||
// Overlay Sync Service - create singleton instance directly
|
||||
const lifecycleEmitter = browserAutomation as unknown as IAutomationLifecycleEmitter;
|
||||
const lifecycleEmitter: IAutomationLifecycleEmitter = {
|
||||
onLifecycle: (cb) => {
|
||||
if ('onLifecycle' in browserAutomation && typeof (browserAutomation as { onLifecycle?: unknown }).onLifecycle === 'function') {
|
||||
(browserAutomation as IAutomationLifecycleEmitter).onLifecycle(cb);
|
||||
}
|
||||
},
|
||||
offLifecycle: (cb) => {
|
||||
if ('offLifecycle' in browserAutomation && typeof (browserAutomation as { offLifecycle?: unknown }).offLifecycle === 'function') {
|
||||
(browserAutomation as IAutomationLifecycleEmitter).offLifecycle(cb);
|
||||
}
|
||||
},
|
||||
};
|
||||
const publisher = {
|
||||
publish: async (event: unknown) => {
|
||||
try {
|
||||
|
||||
@@ -374,9 +374,10 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
||||
// Subscribe to automation adapter lifecycle events and relay to renderer
|
||||
try {
|
||||
if (!lifecycleSubscribed) {
|
||||
const lifecycleEmitter = container.getBrowserAutomation() as unknown as IAutomationLifecycleEmitter;
|
||||
if (typeof lifecycleEmitter.onLifecycle === 'function') {
|
||||
lifecycleEmitter.onLifecycle((ev) => {
|
||||
const browserAutomation = container.getBrowserAutomation();
|
||||
const candidate = browserAutomation as Partial<IAutomationLifecycleEmitter>;
|
||||
if (typeof candidate.onLifecycle === 'function' && typeof candidate.offLifecycle === 'function') {
|
||||
candidate.onLifecycle((ev) => {
|
||||
try {
|
||||
if (mainWindow && mainWindow.webContents) {
|
||||
mainWindow.webContents.send('automation-event', ev);
|
||||
@@ -388,6 +389,8 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
||||
});
|
||||
lifecycleSubscribed = true;
|
||||
logger.debug('Subscribed to adapter lifecycle events for renderer relay');
|
||||
} else {
|
||||
logger.debug?.('Browser automation does not expose lifecycle events; skipping subscription');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -31,12 +31,16 @@ export interface CheckoutConfirmationRequest {
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
export interface StartAutomationResponse {
|
||||
success: boolean;
|
||||
sessionId?: string;
|
||||
error?: string;
|
||||
authRequired?: boolean;
|
||||
authState?: AuthenticationState;
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
startAutomation: (config: HostedSessionConfig) => Promise<{
|
||||
success: boolean;
|
||||
sessionId?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
startAutomation: (config: HostedSessionConfig) => Promise<StartAutomationResponse>;
|
||||
stopAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
getSessionStatus: (sessionId: string) => Promise<any>;
|
||||
pauseAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
@@ -60,7 +64,8 @@ export interface ElectronAPI {
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
startAutomation: (config: HostedSessionConfig) => ipcRenderer.invoke('start-automation', config),
|
||||
startAutomation: (config: HostedSessionConfig) =>
|
||||
ipcRenderer.invoke('start-automation', config) as Promise<StartAutomationResponse>,
|
||||
stopAutomation: (sessionId: string) => ipcRenderer.invoke('stop-automation', sessionId),
|
||||
getSessionStatus: (sessionId: string) => ipcRenderer.invoke('get-session-status', sessionId),
|
||||
pauseAutomation: (sessionId: string) => ipcRenderer.invoke('pause-automation', sessionId),
|
||||
|
||||
@@ -6,6 +6,8 @@ import { BrowserModeToggle } from './components/BrowserModeToggle';
|
||||
import { CheckoutConfirmationDialog } from './components/CheckoutConfirmationDialog';
|
||||
import { RaceCreationSuccessScreen } from './components/RaceCreationSuccessScreen';
|
||||
import type { HostedSessionConfig } from '../../../packages/automation/domain/types/HostedSessionConfig';
|
||||
import type { AuthenticationState } from '../../../packages/automation/domain/value-objects/AuthenticationState';
|
||||
import type { StartAutomationResponse } from '../main/preload';
|
||||
|
||||
interface SessionProgress {
|
||||
sessionId: string;
|
||||
@@ -16,7 +18,7 @@ interface SessionProgress {
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
type AuthState = 'UNKNOWN' | 'AUTHENTICATED' | 'EXPIRED' | 'LOGGED_OUT' | 'CHECKING';
|
||||
type AuthState = AuthenticationState | 'CHECKING';
|
||||
type LoginStatus = 'idle' | 'waiting' | 'success' | 'error';
|
||||
|
||||
export function App() {
|
||||
@@ -84,7 +86,7 @@ export function App() {
|
||||
try {
|
||||
const result = await window.electronAPI.checkAuth();
|
||||
if (result.success && result.state) {
|
||||
setAuthState(result.state as AuthState);
|
||||
setAuthState(result.state);
|
||||
} else {
|
||||
setAuthError(result.error);
|
||||
setAuthState('UNKNOWN');
|
||||
@@ -103,7 +105,7 @@ export function App() {
|
||||
try {
|
||||
const result = await window.electronAPI.checkAuth();
|
||||
if (result.success && result.state) {
|
||||
setAuthState(result.state as AuthState);
|
||||
setAuthState(result.state);
|
||||
} else {
|
||||
setAuthError(result.error || 'Failed to check authentication');
|
||||
setAuthState('UNKNOWN');
|
||||
@@ -138,13 +140,7 @@ export function App() {
|
||||
|
||||
const handleStartAutomation = async (config: HostedSessionConfig) => {
|
||||
setIsRunning(true);
|
||||
const result = await window.electronAPI.startAutomation(config) as {
|
||||
success: boolean;
|
||||
sessionId?: string;
|
||||
error?: string;
|
||||
authRequired?: boolean;
|
||||
authState?: AuthState;
|
||||
};
|
||||
const result: StartAutomationResponse = await window.electronAPI.startAutomation(config);
|
||||
|
||||
if (result.success && result.sessionId) {
|
||||
setSessionId(result.sessionId);
|
||||
@@ -153,8 +149,8 @@ export function App() {
|
||||
|
||||
setIsRunning(false);
|
||||
|
||||
if ('authRequired' in result && result.authRequired) {
|
||||
const nextAuthState = result.authState as AuthState | undefined;
|
||||
if (result.authRequired) {
|
||||
const nextAuthState = result.authState;
|
||||
setAuthState(nextAuthState ?? 'EXPIRED');
|
||||
setAuthError(result.error ?? 'Authentication required before starting automation.');
|
||||
return;
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const presenter = new SponsorDashboardPresenter();
|
||||
const useCase = getGetSponsorDashboardUseCase();
|
||||
await useCase.execute({ sponsorId });
|
||||
await useCase.execute({ sponsorId }, presenter);
|
||||
const dashboard = presenter.getData();
|
||||
|
||||
if (!dashboard) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const presenter = new SponsorSponsorshipsPresenter();
|
||||
const useCase = getGetSponsorSponsorshipsUseCase();
|
||||
await useCase.execute({ sponsorId });
|
||||
await useCase.execute({ sponsorId }, presenter);
|
||||
const sponsorships = presenter.getData();
|
||||
|
||||
if (!sponsorships) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useState, FormEvent, type ChangeEvent } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
@@ -145,7 +145,7 @@ export default function LoginPage() {
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, email: e.target.value })}
|
||||
error={!!errors.email}
|
||||
errorMessage={errors.email}
|
||||
placeholder="you@example.com"
|
||||
@@ -172,7 +172,7 @@ export default function LoginPage() {
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, password: e.target.value })}
|
||||
error={!!errors.password}
|
||||
errorMessage={errors.password}
|
||||
placeholder="••••••••"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, FormEvent } from 'react';
|
||||
import { useState, useEffect, FormEvent, type ChangeEvent } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
@@ -226,7 +226,7 @@ export default function SignupPage() {
|
||||
id="displayName"
|
||||
type="text"
|
||||
value={formData.displayName}
|
||||
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, displayName: e.target.value })}
|
||||
error={!!errors.displayName}
|
||||
errorMessage={errors.displayName}
|
||||
placeholder="SpeedyRacer42"
|
||||
@@ -249,7 +249,7 @@ export default function SignupPage() {
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, email: e.target.value })}
|
||||
error={!!errors.email}
|
||||
errorMessage={errors.email}
|
||||
placeholder="you@example.com"
|
||||
@@ -271,7 +271,7 @@ export default function SignupPage() {
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, password: e.target.value })}
|
||||
error={!!errors.password}
|
||||
errorMessage={errors.password}
|
||||
placeholder="••••••••"
|
||||
@@ -336,7 +336,7 @@ export default function SignupPage() {
|
||||
id="confirmPassword"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
error={!!errors.confirmPassword}
|
||||
errorMessage={errors.confirmPassword}
|
||||
placeholder="••••••••"
|
||||
|
||||
@@ -27,6 +27,7 @@ import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { getAuthService } from '@/lib/auth';
|
||||
import { getGetDashboardOverviewUseCase } from '@/lib/di-container';
|
||||
import { DashboardOverviewPresenter } from '@/lib/presenters/DashboardOverviewPresenter';
|
||||
import type {
|
||||
DashboardOverviewViewModel,
|
||||
DashboardFeedItemSummaryViewModel,
|
||||
@@ -94,8 +95,9 @@ export default async function DashboardPage() {
|
||||
const currentDriverId = session.user.primaryDriverId ?? '';
|
||||
|
||||
const useCase = getGetDashboardOverviewUseCase();
|
||||
await useCase.execute({ driverId: currentDriverId });
|
||||
const viewModel = useCase.presenter.getViewModel() as DashboardOverviewViewModel | null;
|
||||
const presenter = new DashboardOverviewPresenter();
|
||||
await useCase.execute({ driverId: currentDriverId }, presenter);
|
||||
const viewModel = presenter.getViewModel();
|
||||
|
||||
if (!viewModel) {
|
||||
return null;
|
||||
|
||||
@@ -28,6 +28,7 @@ import Input from '@/components/ui/Input';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
|
||||
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter';
|
||||
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -387,13 +388,16 @@ export default function DriversPage() {
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const useCase = getGetDriversLeaderboardUseCase();
|
||||
await useCase.execute();
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
const presenter = new DriversLeaderboardPresenter();
|
||||
await useCase.execute(undefined as void, presenter);
|
||||
const viewModel = presenter.getViewModel();
|
||||
|
||||
setDrivers(viewModel.drivers);
|
||||
setTotalRaces(viewModel.totalRaces);
|
||||
setTotalWins(viewModel.totalWins);
|
||||
setActiveCount(viewModel.activeCount);
|
||||
if (viewModel) {
|
||||
setDrivers(viewModel.drivers);
|
||||
setTotalRaces(viewModel.totalRaces);
|
||||
setTotalWins(viewModel.totalWins);
|
||||
setActiveCount(viewModel.activeCount);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
|
||||
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter';
|
||||
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -181,9 +182,12 @@ export default function DriverLeaderboardPage() {
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const useCase = getGetDriversLeaderboardUseCase();
|
||||
await useCase.execute();
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
setDrivers(viewModel.drivers);
|
||||
const presenter = new DriversLeaderboardPresenter();
|
||||
await useCase.execute(undefined as void, presenter);
|
||||
const viewModel = presenter.getViewModel();
|
||||
if (viewModel) {
|
||||
setDrivers(viewModel.drivers);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import Button from '@/components/ui/Button';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getGetDriversLeaderboardUseCase, getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
|
||||
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter';
|
||||
import { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter';
|
||||
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import type { TeamLeaderboardItemViewModel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
@@ -287,15 +288,16 @@ export default function LeaderboardsPage() {
|
||||
try {
|
||||
const driversUseCase = getGetDriversLeaderboardUseCase();
|
||||
const teamsUseCase = getGetTeamsLeaderboardUseCase();
|
||||
const driversPresenter = new DriversLeaderboardPresenter();
|
||||
const teamsPresenter = new TeamsLeaderboardPresenter();
|
||||
|
||||
await driversUseCase.execute();
|
||||
await driversUseCase.execute(undefined as void, driversPresenter);
|
||||
await teamsUseCase.execute(undefined as void, teamsPresenter);
|
||||
|
||||
const driversViewModel = driversUseCase.presenter.getViewModel();
|
||||
const driversViewModel = driversPresenter.getViewModel();
|
||||
const teamsViewModel = teamsPresenter.getViewModel();
|
||||
|
||||
setDrivers(driversViewModel.drivers);
|
||||
setDrivers(driversViewModel?.drivers ?? []);
|
||||
setTeams(teamsViewModel ? teamsViewModel.teams : []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load leaderboard data:', error);
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
getSponsorRepository,
|
||||
getSeasonSponsorshipRepository,
|
||||
} from '@/lib/di-container';
|
||||
import { LeagueScoringConfigPresenter } from '@/lib/presenters/LeagueScoringConfigPresenter';
|
||||
import { Trophy, Star, ExternalLink } from 'lucide-react';
|
||||
import { getMembership, getLeagueMembers } from '@/lib/leagueMembership';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
@@ -125,8 +126,9 @@ export default function LeagueDetailPage() {
|
||||
|
||||
// Load scoring configuration for the active season
|
||||
const getLeagueScoringConfigUseCase = getGetLeagueScoringConfigUseCase();
|
||||
await getLeagueScoringConfigUseCase.execute({ leagueId });
|
||||
const scoringViewModel = getLeagueScoringConfigUseCase.presenter.getViewModel();
|
||||
const scoringPresenter = new LeagueScoringConfigPresenter();
|
||||
await getLeagueScoringConfigUseCase.execute({ leagueId }, scoringPresenter);
|
||||
const scoringViewModel = scoringPresenter.getViewModel();
|
||||
setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO);
|
||||
|
||||
// Load all drivers for standings and map to DTOs for UI components
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getLeagueRepository,
|
||||
getGetLeagueScoringConfigUseCase
|
||||
} from '@/lib/di-container';
|
||||
import { LeagueScoringConfigPresenter } from '@/lib/presenters/LeagueScoringConfigPresenter';
|
||||
import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO';
|
||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
||||
|
||||
@@ -35,8 +36,9 @@ export default function LeagueRulebookPage() {
|
||||
|
||||
setLeague(leagueData);
|
||||
|
||||
await scoringUseCase.execute({ leagueId });
|
||||
const scoringViewModel = scoringUseCase.presenter.getViewModel();
|
||||
const scoringPresenter = new LeagueScoringConfigPresenter();
|
||||
await scoringUseCase.execute({ leagueId }, scoringPresenter);
|
||||
const scoringViewModel = scoringPresenter.getViewModel();
|
||||
setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO);
|
||||
} catch (err) {
|
||||
console.error('Failed to load scoring config:', err);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getDriverRepository,
|
||||
getLeagueMembershipRepository
|
||||
} from '@/lib/di-container';
|
||||
import { LeagueDriverSeasonStatsPresenter } from '@/lib/presenters/LeagueDriverSeasonStatsPresenter';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
|
||||
import type { MembershipRole, LeagueMembership } from '@/lib/leagueMembership';
|
||||
@@ -36,15 +37,9 @@ export default function LeagueStandingsPage() {
|
||||
const driverRepo = getDriverRepository();
|
||||
const membershipRepo = getLeagueMembershipRepository();
|
||||
|
||||
await getLeagueDriverSeasonStatsUseCase.execute({ leagueId });
|
||||
type GetLeagueDriverSeasonStatsUseCaseType = {
|
||||
presenter: {
|
||||
getViewModel(): { stats: LeagueDriverSeasonStatsDTO[] };
|
||||
};
|
||||
};
|
||||
const typedUseCase =
|
||||
getLeagueDriverSeasonStatsUseCase as GetLeagueDriverSeasonStatsUseCaseType;
|
||||
const standingsViewModel = typedUseCase.presenter.getViewModel();
|
||||
const presenter = new LeagueDriverSeasonStatsPresenter();
|
||||
await getLeagueDriverSeasonStatsUseCase.execute({ leagueId }, presenter);
|
||||
const standingsViewModel = presenter.getViewModel();
|
||||
setStandings(standingsViewModel.stats);
|
||||
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
|
||||
@@ -30,7 +30,8 @@ import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import type { LeagueSummaryDTO } from '@gridpilot/racing/application/dto/LeagueSummaryDTO';
|
||||
import type { LeagueSummaryViewModel } from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
import { AllLeaguesWithCapacityAndScoringPresenter } from '@/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter';
|
||||
import { getGetAllLeaguesWithCapacityAndScoringUseCase } from '@/lib/di-container';
|
||||
|
||||
// ============================================================================
|
||||
@@ -57,7 +58,7 @@ interface Category {
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
description: string;
|
||||
filter: (league: LeagueSummaryDTO) => boolean;
|
||||
filter: (league: LeagueSummaryViewModel) => boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
@@ -175,7 +176,7 @@ interface LeagueSliderProps {
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
description: string;
|
||||
leagues: LeagueSummaryDTO[];
|
||||
leagues: LeagueSummaryViewModel[];
|
||||
onLeagueClick: (id: string) => void;
|
||||
autoScroll?: boolean;
|
||||
iconColor?: string;
|
||||
@@ -377,25 +378,23 @@ function LeagueSlider({
|
||||
|
||||
export default function LeaguesPage() {
|
||||
const router = useRouter();
|
||||
const [realLeagues, setRealLeagues] = useState<LeagueSummaryDTO[]>([]);
|
||||
const [realLeagues, setRealLeagues] = useState<LeagueSummaryViewModel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeCategory, setActiveCategory] = useState<CategoryId>('all');
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadLeagues();
|
||||
void loadLeagues();
|
||||
}, []);
|
||||
|
||||
const loadLeagues = async () => {
|
||||
try {
|
||||
const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase();
|
||||
await useCase.execute();
|
||||
const presenter = useCase.presenter as unknown as {
|
||||
getViewModel(): { leagues: LeagueSummaryDTO[] };
|
||||
};
|
||||
const presenter = new AllLeaguesWithCapacityAndScoringPresenter();
|
||||
await useCase.execute(undefined as void, presenter);
|
||||
const viewModel = presenter.getViewModel();
|
||||
setRealLeagues(viewModel.leagues);
|
||||
setRealLeagues(viewModel?.leagues ?? []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load leagues:', error);
|
||||
} finally {
|
||||
@@ -434,7 +433,7 @@ export default function LeaguesPage() {
|
||||
acc[category.id] = searchFilteredLeagues.filter(category.filter);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<CategoryId, LeagueSummaryDTO[]>,
|
||||
{} as Record<CategoryId, LeagueSummaryViewModel[]>,
|
||||
);
|
||||
|
||||
// Featured categories to show as sliders with different scroll speeds and alternating directions
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
ArrowLeft,
|
||||
Scale,
|
||||
} from 'lucide-react';
|
||||
import { RaceDetailPresenter } from '@/lib/presenters/RaceDetailPresenter';
|
||||
|
||||
export default function RaceDetailPage() {
|
||||
const router = useRouter();
|
||||
@@ -57,8 +58,9 @@ export default function RaceDetailPage() {
|
||||
setError(null);
|
||||
try {
|
||||
const useCase = getGetRaceDetailUseCase();
|
||||
await useCase.execute({ raceId, driverId: currentDriverId });
|
||||
const vm = useCase.presenter.getViewModel();
|
||||
const presenter = new RaceDetailPresenter();
|
||||
await useCase.execute({ raceId, driverId: currentDriverId }, presenter);
|
||||
const vm = presenter.getViewModel();
|
||||
if (!vm) {
|
||||
throw new Error('Race detail not available');
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
getGetRaceResultsDetailUseCase,
|
||||
getImportRaceResultsUseCase,
|
||||
} from '@/lib/di-container';
|
||||
import { RaceWithSOFPresenter } from '@/lib/presenters/RaceWithSOFPresenter';
|
||||
import { RaceResultsDetailPresenter } from '@/lib/presenters/RaceResultsDetailPresenter';
|
||||
import type {
|
||||
RaceResultsHeaderViewModel,
|
||||
RaceResultsLeagueViewModel,
|
||||
@@ -71,7 +73,7 @@ export default function RaceResultsPage() {
|
||||
const [currentDriverId, setCurrentDriverId] = useState<string | undefined>(undefined);
|
||||
const [raceSOF, setRaceSOF] = useState<number | null>(null);
|
||||
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 [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -81,9 +83,10 @@ export default function RaceResultsPage() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const raceResultsUseCase = getGetRaceResultsDetailUseCase();
|
||||
await raceResultsUseCase.execute({ raceId });
|
||||
const raceResultsPresenter = new RaceResultsDetailPresenter();
|
||||
await raceResultsUseCase.execute({ raceId }, raceResultsPresenter);
|
||||
|
||||
const viewModel = raceResultsUseCase.presenter.getViewModel();
|
||||
const viewModel = raceResultsPresenter.getViewModel();
|
||||
|
||||
if (!viewModel) {
|
||||
setError('Failed to load race data');
|
||||
@@ -130,8 +133,9 @@ export default function RaceResultsPage() {
|
||||
|
||||
try {
|
||||
const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
|
||||
await raceWithSOFUseCase.execute({ raceId });
|
||||
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel();
|
||||
const sofPresenter = new RaceWithSOFPresenter();
|
||||
await raceWithSOFUseCase.execute({ raceId }, sofPresenter);
|
||||
const raceViewModel = sofPresenter.getViewModel();
|
||||
if (raceViewModel) {
|
||||
setRaceSOF(raceViewModel.strengthOfField);
|
||||
}
|
||||
@@ -290,7 +294,7 @@ export default function RaceResultsPage() {
|
||||
<ResultsTable
|
||||
results={results}
|
||||
drivers={drivers}
|
||||
pointsSystem={pointsSystem}
|
||||
pointsSystem={pointsSystem ?? {}}
|
||||
fastestLapTime={fastestLapTime ?? 0}
|
||||
penalties={penalties}
|
||||
currentDriverId={currentDriverId ?? ''}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Button from '@/components/ui/Button';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import Breadcrumbs from '@/components/layout/Breadcrumbs';
|
||||
import { getGetAllRacesPageDataUseCase } from '@/lib/di-container';
|
||||
import { AllRacesPagePresenter } from '@/lib/presenters/AllRacesPagePresenter';
|
||||
import type {
|
||||
AllRacesPageViewModel,
|
||||
AllRacesListItemViewModel,
|
||||
@@ -53,8 +54,9 @@ export default function AllRacesPage() {
|
||||
const loadRaces = async () => {
|
||||
try {
|
||||
const useCase = getGetAllRacesPageDataUseCase();
|
||||
await useCase.execute();
|
||||
const viewModel = useCase.presenter.getViewModel();
|
||||
const presenter = new AllRacesPagePresenter();
|
||||
await useCase.execute(undefined, presenter);
|
||||
const viewModel = presenter.getViewModel();
|
||||
setPageData(viewModel);
|
||||
} catch (err) {
|
||||
console.error('Failed to load races:', err);
|
||||
|
||||
@@ -7,6 +7,7 @@ import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { getGetRacesPageDataUseCase } from '@/lib/di-container';
|
||||
import { RacesPagePresenter } from '@/lib/presenters/RacesPagePresenter';
|
||||
import type {
|
||||
RacesPageViewModel,
|
||||
RaceListItemViewModel,
|
||||
@@ -46,8 +47,9 @@ export default function RacesPage() {
|
||||
const loadRaces = async () => {
|
||||
try {
|
||||
const useCase = getGetRacesPageDataUseCase();
|
||||
await useCase.execute();
|
||||
const data = useCase.presenter.getViewModel();
|
||||
const presenter = new RacesPagePresenter();
|
||||
await useCase.execute(undefined, presenter);
|
||||
const data = presenter.getViewModel();
|
||||
setPageData(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load races:', err);
|
||||
|
||||
@@ -10,6 +10,8 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs';
|
||||
import SponsorInsightsCard, { useSponsorMode, MetricBuilders, SlotTemplates } from '@/components/sponsors/SponsorInsightsCard';
|
||||
import { getImageService } from '@/lib/di-container';
|
||||
import { TeamMembersPresenter } from '@/lib/presenters/TeamMembersPresenter';
|
||||
import { TeamDetailsPresenter } from '@/lib/presenters/TeamDetailsPresenter';
|
||||
import type { TeamDetailsViewModel } from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
|
||||
import TeamRoster from '@/components/teams/TeamRoster';
|
||||
import TeamStandings from '@/components/teams/TeamStandings';
|
||||
import TeamAdmin from '@/components/teams/TeamAdmin';
|
||||
@@ -20,7 +22,6 @@ import {
|
||||
getTeamMembershipRepository,
|
||||
} from '@/lib/di-container';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
import type { Team } from '@gridpilot/racing';
|
||||
import { Users, Trophy, TrendingUp, Star, Zap } from 'lucide-react';
|
||||
|
||||
type TeamRole = 'owner' | 'manager' | 'driver';
|
||||
@@ -36,8 +37,10 @@ type Tab = 'overview' | 'roster' | 'standings' | 'admin';
|
||||
export default function TeamDetailPage() {
|
||||
const params = useParams();
|
||||
const teamId = params.id as string;
|
||||
|
||||
const [team, setTeam] = useState<Team | null>(null);
|
||||
|
||||
type TeamViewModel = TeamDetailsViewModel['team'];
|
||||
|
||||
const [team, setTeam] = useState<TeamViewModel | null>(null);
|
||||
const [memberships, setMemberships] = useState<TeamMembership[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<Tab>('overview');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -51,11 +54,9 @@ export default function TeamDetailPage() {
|
||||
const detailsUseCase = getGetTeamDetailsUseCase();
|
||||
const membersUseCase = getGetTeamMembersUseCase();
|
||||
|
||||
await detailsUseCase.execute(teamId, currentDriverId);
|
||||
const detailsPresenter = detailsUseCase.presenter;
|
||||
const detailsViewModel = detailsPresenter
|
||||
? (detailsPresenter as any).getViewModel?.() as { team: Team } | null
|
||||
: null;
|
||||
const detailsPresenter = new TeamDetailsPresenter();
|
||||
await detailsUseCase.execute({ teamId, driverId: currentDriverId }, detailsPresenter);
|
||||
const detailsViewModel = detailsPresenter.getViewModel();
|
||||
|
||||
if (!detailsViewModel) {
|
||||
setTeam(null);
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import type { LeagueSummaryDTO } from '@gridpilot/racing/application/dto/LeagueSummaryDTO';
|
||||
import type { LeagueSummaryViewModel } from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
import { getLeagueCoverClasses } from '@/lib/leagueCovers';
|
||||
import { getImageService } from '@/lib/di-container';
|
||||
|
||||
interface LeagueCardProps {
|
||||
league: LeagueSummaryDTO;
|
||||
league: LeagueSummaryViewModel;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ function getGameColor(gameId?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function isNewLeague(createdAt: Date): boolean {
|
||||
function isNewLeague(createdAt: string | Date): boolean {
|
||||
const oneWeekAgo = new Date();
|
||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||
return new Date(createdAt) > oneWeekAgo;
|
||||
|
||||
@@ -251,7 +251,7 @@ export default function DriverProfileMockup() {
|
||||
|
||||
function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boolean; value: number }) {
|
||||
const count = useMotionValue(0);
|
||||
const rounded = useTransform(count, (v) => Math.round(v));
|
||||
const rounded = useTransform(count, (v: number) => Math.round(v));
|
||||
const spring = useSpring(count, { stiffness: 50, damping: 25 });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -282,7 +282,7 @@ function AnimatedCounter({
|
||||
suffix?: string;
|
||||
}) {
|
||||
const count = useMotionValue(0);
|
||||
const rounded = useTransform(count, (v) => Math.round(v));
|
||||
const rounded = useTransform(count, (v: number) => Math.round(v));
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldReduceMotion) {
|
||||
|
||||
@@ -146,7 +146,7 @@ function RatingFactor({
|
||||
}) {
|
||||
const progress = useMotionValue(0);
|
||||
const smoothProgress = useSpring(progress, { stiffness: 60, damping: 25 });
|
||||
const width = useTransform(smoothProgress, (v) => `${v}%`);
|
||||
const width = useTransform(smoothProgress, (v: number) => `${v}%`);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldReduceMotion) {
|
||||
@@ -187,7 +187,7 @@ function RatingFactor({
|
||||
|
||||
function AnimatedRating({ shouldReduceMotion }: { shouldReduceMotion: boolean }) {
|
||||
const count = useMotionValue(0);
|
||||
const rounded = useTransform(count, (v) => Math.round(v));
|
||||
const rounded = useTransform(count, (v: number) => Math.round(v));
|
||||
const spring = useSpring(count, { stiffness: 50, damping: 25 });
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
49
apps/website/env.d.ts
vendored
49
apps/website/env.d.ts
vendored
@@ -1,6 +1,55 @@
|
||||
/// <reference types="next" />
|
||||
/// <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 {
|
||||
interface ProcessEnv {
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha';
|
||||
|
||||
@@ -195,15 +195,15 @@ export function configureDIContainer(): void {
|
||||
|
||||
// Create driver statistics from seed data
|
||||
type DemoDriverStatsEntry = {
|
||||
rating?: number;
|
||||
wins?: number;
|
||||
podiums?: number;
|
||||
rating: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
totalRaces: number;
|
||||
overallRank: number | null;
|
||||
dnfs?: number;
|
||||
totalRaces?: number;
|
||||
avgFinish?: number;
|
||||
bestFinish?: number;
|
||||
worstFinish?: number;
|
||||
overallRank?: number;
|
||||
consistency?: number;
|
||||
percentile?: number;
|
||||
driverId?: string;
|
||||
@@ -952,10 +952,9 @@ export function configureDIContainer(): void {
|
||||
new IsDriverRegisteredForRaceUseCase(raceRegistrationRepository, driverRegistrationStatusPresenter)
|
||||
);
|
||||
|
||||
const raceRegistrationsPresenter = new RaceRegistrationsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRaceRegistrationsUseCase,
|
||||
new GetRaceRegistrationsUseCase(raceRegistrationRepository, raceRegistrationsPresenter)
|
||||
new GetRaceRegistrationsUseCase(raceRegistrationRepository)
|
||||
);
|
||||
|
||||
const leagueStandingsPresenter = new LeagueStandingsPresenter();
|
||||
@@ -964,7 +963,6 @@ export function configureDIContainer(): void {
|
||||
new GetLeagueStandingsUseCase(standingRepository),
|
||||
);
|
||||
|
||||
const leagueDriverSeasonStatsPresenter = new LeagueDriverSeasonStatsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetLeagueDriverSeasonStatsUseCase,
|
||||
new GetLeagueDriverSeasonStatsUseCase(
|
||||
@@ -986,21 +984,17 @@ export function configureDIContainer(): void {
|
||||
};
|
||||
},
|
||||
},
|
||||
leagueDriverSeasonStatsPresenter,
|
||||
),
|
||||
);
|
||||
|
||||
const allLeaguesWithCapacityPresenter = new AllLeaguesWithCapacityPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetAllLeaguesWithCapacityUseCase,
|
||||
new GetAllLeaguesWithCapacityUseCase(
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
allLeaguesWithCapacityPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const allLeaguesWithCapacityAndScoringPresenter = new AllLeaguesWithCapacityAndScoringPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase,
|
||||
new GetAllLeaguesWithCapacityAndScoringUseCase(
|
||||
@@ -1010,7 +1004,6 @@ export function configureDIContainer(): void {
|
||||
leagueScoringConfigRepository,
|
||||
gameRepository,
|
||||
leagueScoringPresetProvider,
|
||||
allLeaguesWithCapacityAndScoringPresenter
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1020,7 +1013,6 @@ export function configureDIContainer(): void {
|
||||
new ListLeagueScoringPresetsUseCase(leagueScoringPresetProvider)
|
||||
);
|
||||
|
||||
const leagueScoringConfigPresenter = new LeagueScoringConfigPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetLeagueScoringConfigUseCase,
|
||||
new GetLeagueScoringConfigUseCase(
|
||||
@@ -1029,7 +1021,6 @@ export function configureDIContainer(): void {
|
||||
leagueScoringConfigRepository,
|
||||
gameRepository,
|
||||
leagueScoringPresetProvider,
|
||||
leagueScoringConfigPresenter
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1049,7 +1040,6 @@ export function configureDIContainer(): void {
|
||||
new PreviewLeagueScheduleUseCase(undefined, leagueSchedulePreviewPresenter),
|
||||
);
|
||||
|
||||
const raceWithSOFPresenter = new RaceWithSOFPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRaceWithSOFUseCase,
|
||||
new GetRaceWithSOFUseCase(
|
||||
@@ -1057,7 +1047,6 @@ export function configureDIContainer(): void {
|
||||
raceRegistrationRepository,
|
||||
resultRepository,
|
||||
driverRatingProvider,
|
||||
raceWithSOFPresenter
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1073,21 +1062,18 @@ export function configureDIContainer(): void {
|
||||
)
|
||||
);
|
||||
|
||||
const racesPresenter = new RacesPagePresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRacesPageDataUseCase,
|
||||
new GetRacesPageDataUseCase(raceRepository, leagueRepository, racesPresenter)
|
||||
new GetRacesPageDataUseCase(raceRepository, leagueRepository)
|
||||
);
|
||||
|
||||
const allRacesPagePresenter = new AllRacesPagePresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetAllRacesPageDataUseCase,
|
||||
new GetAllRacesPageDataUseCase(raceRepository, leagueRepository, allRacesPagePresenter)
|
||||
new GetAllRacesPageDataUseCase(raceRepository, leagueRepository)
|
||||
);
|
||||
|
||||
const imageService = container.resolve<ImageServicePort>(DI_TOKENS.ImageService);
|
||||
|
||||
const raceDetailPresenter = new RaceDetailPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRaceDetailUseCase,
|
||||
new GetRaceDetailUseCase(
|
||||
@@ -1099,11 +1085,9 @@ export function configureDIContainer(): void {
|
||||
leagueMembershipRepository,
|
||||
driverRatingProvider,
|
||||
imageService,
|
||||
raceDetailPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const raceResultsDetailPresenter = new RaceResultsDetailPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetRaceResultsDetailUseCase,
|
||||
new GetRaceResultsDetailUseCase(
|
||||
@@ -1112,7 +1096,6 @@ export function configureDIContainer(): void {
|
||||
resultRepository,
|
||||
driverRepository,
|
||||
penaltyRepository,
|
||||
raceResultsDetailPresenter
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1149,7 +1132,6 @@ export function configureDIContainer(): void {
|
||||
},
|
||||
};
|
||||
|
||||
const driversPresenter = new DriversLeaderboardPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetDriversLeaderboardUseCase,
|
||||
new GetDriversLeaderboardUseCase(
|
||||
@@ -1157,7 +1139,6 @@ export function configureDIContainer(): void {
|
||||
rankingService,
|
||||
driverStatsService,
|
||||
imageService,
|
||||
driversPresenter
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1215,7 +1196,6 @@ export function configureDIContainer(): void {
|
||||
};
|
||||
};
|
||||
|
||||
const dashboardOverviewPresenter = new DashboardOverviewPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetDashboardOverviewUseCase,
|
||||
new GetDashboardOverviewUseCase(
|
||||
@@ -1230,7 +1210,6 @@ export function configureDIContainer(): void {
|
||||
socialRepository,
|
||||
imageService,
|
||||
getDriverStatsForDashboard,
|
||||
dashboardOverviewPresenter
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1260,11 +1239,10 @@ export function configureDIContainer(): void {
|
||||
DI_TOKENS.GetAllTeamsUseCase,
|
||||
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository),
|
||||
);
|
||||
|
||||
const teamDetailsPresenter = new TeamDetailsPresenter();
|
||||
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetTeamDetailsUseCase,
|
||||
new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository, teamDetailsPresenter)
|
||||
new GetTeamDetailsUseCase(teamRepository, teamMembershipRepository)
|
||||
);
|
||||
|
||||
const teamMembersPresenter = new TeamMembersPresenter();
|
||||
@@ -1313,7 +1291,6 @@ export function configureDIContainer(): void {
|
||||
const sponsorRepository = container.resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository);
|
||||
const seasonSponsorshipRepository = container.resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
|
||||
|
||||
const sponsorDashboardPresenter = new SponsorDashboardPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetSponsorDashboardUseCase,
|
||||
new GetSponsorDashboardUseCase(
|
||||
@@ -1323,11 +1300,9 @@ export function configureDIContainer(): void {
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRepository,
|
||||
sponsorDashboardPresenter
|
||||
)
|
||||
);
|
||||
|
||||
const sponsorSponsorshipsPresenter = new SponsorSponsorshipsPresenter();
|
||||
container.registerInstance(
|
||||
DI_TOKENS.GetSponsorSponsorshipsUseCase,
|
||||
new GetSponsorSponsorshipsUseCase(
|
||||
@@ -1337,7 +1312,6 @@ export function configureDIContainer(): void {
|
||||
leagueRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRepository,
|
||||
sponsorSponsorshipsPresenter
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -625,7 +625,7 @@ export function getIsDriverRegisteredForRaceQuery(): {
|
||||
return {
|
||||
async execute(input: { raceId: string; driverId: string }): Promise<boolean> {
|
||||
const result = await useCase.execute(input);
|
||||
return result as unknown as boolean;
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type {
|
||||
IAllLeaguesWithCapacityAndScoringPresenter,
|
||||
LeagueEnrichedData,
|
||||
@@ -9,7 +8,11 @@ import type {
|
||||
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
|
||||
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null;
|
||||
|
||||
present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(enrichedLeagues: LeagueEnrichedData[]): void {
|
||||
const leagueItems: LeagueSummaryViewModel[] = enrichedLeagues.map((data) => {
|
||||
const { league, usedDriverSlots, season, scoringConfig, game, preset } = data;
|
||||
|
||||
@@ -68,7 +71,7 @@ export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWit
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
createdAt: league.createdAt,
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
maxDrivers: safeMaxDrivers,
|
||||
usedDriverSlots,
|
||||
// Team capacity is not yet modeled here; use zero for now to satisfy strict typing.
|
||||
@@ -87,14 +90,9 @@ export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWit
|
||||
leagues: leagueItems,
|
||||
totalCount: leagueItems.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type {
|
||||
IAllLeaguesWithCapacityPresenter,
|
||||
LeagueWithCapacityViewModel,
|
||||
AllLeaguesWithCapacityViewModel,
|
||||
AllLeaguesWithCapacityResultDTO,
|
||||
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
|
||||
|
||||
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter {
|
||||
private viewModel: AllLeaguesWithCapacityViewModel | null = null;
|
||||
|
||||
present(
|
||||
leagues: League[],
|
||||
memberCounts: Map<string, number>
|
||||
): AllLeaguesWithCapacityViewModel {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: AllLeaguesWithCapacityResultDTO): void {
|
||||
const { leagues, memberCounts } = input;
|
||||
const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => {
|
||||
const usedSlots = memberCounts.get(league.id) ?? 0;
|
||||
|
||||
@@ -63,14 +65,9 @@ export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityP
|
||||
leagues: leagueItems,
|
||||
totalCount: leagueItems.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
getViewModel(): AllLeaguesWithCapacityViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import type {
|
||||
IAllRacesPagePresenter,
|
||||
AllRacesPageResultDTO,
|
||||
AllRacesPageViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IAllRacesPagePresenter';
|
||||
|
||||
export class AllRacesPagePresenter implements IAllRacesPagePresenter {
|
||||
private viewModel: AllRacesPageViewModel | null = null;
|
||||
|
||||
present(viewModel: AllRacesPageViewModel): void {
|
||||
this.viewModel = viewModel;
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: AllRacesPageResultDTO): void {
|
||||
this.viewModel = dto;
|
||||
}
|
||||
|
||||
getViewModel(): AllRacesPageViewModel | null {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type {
|
||||
IDashboardOverviewPresenter,
|
||||
DashboardOverviewResultDTO,
|
||||
DashboardOverviewViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IDashboardOverviewPresenter';
|
||||
|
||||
export class DashboardOverviewPresenter implements IDashboardOverviewPresenter {
|
||||
private viewModel: DashboardOverviewViewModel | null = null;
|
||||
|
||||
present(viewModel: DashboardOverviewViewModel): void {
|
||||
this.viewModel = viewModel;
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: DashboardOverviewResultDTO): void {
|
||||
this.viewModel = dto;
|
||||
}
|
||||
|
||||
getViewModel(): DashboardOverviewViewModel | null {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import type { SkillLevel } from '@gridpilot/racing/domain/services/SkillLevelService';
|
||||
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
|
||||
import type {
|
||||
IDriversLeaderboardPresenter,
|
||||
DriverLeaderboardItemViewModel,
|
||||
DriversLeaderboardViewModel,
|
||||
DriversLeaderboardResultDTO,
|
||||
} from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
|
||||
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
||||
private viewModel: DriversLeaderboardViewModel | null = null;
|
||||
|
||||
present(
|
||||
drivers: Driver[],
|
||||
rankings: Array<{ driverId: string; rating: number; overallRank: number }>,
|
||||
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
|
||||
avatarUrls: Record<string, string>
|
||||
): DriversLeaderboardViewModel {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: DriversLeaderboardResultDTO): void {
|
||||
const { drivers, rankings, stats, avatarUrls } = input;
|
||||
const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => {
|
||||
const driverStats = stats[driver.id];
|
||||
const rating = driverStats?.rating ?? 0;
|
||||
@@ -68,14 +67,9 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter
|
||||
totalWins,
|
||||
activeCount,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): DriversLeaderboardViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
getViewModel(): DriversLeaderboardViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,19 @@ import type {
|
||||
ILeagueDriverSeasonStatsPresenter,
|
||||
LeagueDriverSeasonStatsItemViewModel,
|
||||
LeagueDriverSeasonStatsViewModel,
|
||||
LeagueDriverSeasonStatsResultDTO,
|
||||
} from '@gridpilot/racing/application/presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
|
||||
export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter {
|
||||
private viewModel: LeagueDriverSeasonStatsViewModel | null = null;
|
||||
|
||||
present(
|
||||
leagueId: string,
|
||||
standings: Array<{
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
racesCompleted: number;
|
||||
}>,
|
||||
penalties: Map<string, { baseDelta: number; bonusDelta: number }>,
|
||||
driverResults: Map<string, Array<{ position: number }>>,
|
||||
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
|
||||
): LeagueDriverSeasonStatsViewModel {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: LeagueDriverSeasonStatsResultDTO): void {
|
||||
const { leagueId, standings, penalties, driverResults, driverRatings } = dto;
|
||||
|
||||
const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => {
|
||||
const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
const totalPenaltyPoints = penalty.baseDelta;
|
||||
@@ -65,8 +61,6 @@ export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStat
|
||||
leagueId,
|
||||
stats,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueDriverSeasonStatsViewModel {
|
||||
|
||||
@@ -10,6 +10,10 @@ import type {
|
||||
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
|
||||
private viewModel: LeagueScoringConfigViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
|
||||
const championships: LeagueScoringChampionshipViewModel[] =
|
||||
data.championships.map((champ) => this.mapChampionship(champ));
|
||||
|
||||
@@ -6,9 +6,13 @@ import type {
|
||||
export class RaceDetailPresenter implements IRaceDetailPresenter {
|
||||
private viewModel: RaceDetailViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(viewModel: RaceDetailViewModel): RaceDetailViewModel {
|
||||
this.viewModel = viewModel;
|
||||
return this.viewModel;
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceDetailViewModel | null {
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import type {
|
||||
IRaceRegistrationsPresenter,
|
||||
RaceRegistrationsViewModel,
|
||||
RaceRegistrationsResultDTO,
|
||||
} from '@gridpilot/racing/application/presenters/IRaceRegistrationsPresenter';
|
||||
|
||||
export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter {
|
||||
private viewModel: RaceRegistrationsViewModel | null = null;
|
||||
|
||||
present(registeredDriverIds: string[]): RaceRegistrationsViewModel {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: RaceRegistrationsResultDTO): void {
|
||||
const { registeredDriverIds } = input;
|
||||
this.viewModel = {
|
||||
registeredDriverIds,
|
||||
count: registeredDriverIds.length,
|
||||
};
|
||||
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceRegistrationsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
getViewModel(): RaceRegistrationsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@ import type {
|
||||
export class RaceResultsDetailPresenter implements IRaceResultsDetailPresenter {
|
||||
private viewModel: RaceResultsDetailViewModel | null = null;
|
||||
|
||||
present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(viewModel: RaceResultsDetailViewModel): void {
|
||||
this.viewModel = viewModel;
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceResultsDetailViewModel | null {
|
||||
|
||||
@@ -1,49 +1,35 @@
|
||||
import type {
|
||||
IRaceWithSOFPresenter,
|
||||
RaceWithSOFResultDTO,
|
||||
RaceWithSOFViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRaceWithSOFPresenter';
|
||||
|
||||
export class RaceWithSOFPresenter implements IRaceWithSOFPresenter {
|
||||
private viewModel: RaceWithSOFViewModel | null = null;
|
||||
|
||||
present(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
scheduledAt: Date,
|
||||
track: string,
|
||||
trackId: string,
|
||||
car: string,
|
||||
carId: string,
|
||||
sessionType: string,
|
||||
status: string,
|
||||
strengthOfField: number | null,
|
||||
registeredCount: number,
|
||||
maxParticipants: number,
|
||||
participantCount: number
|
||||
): RaceWithSOFViewModel {
|
||||
present(dto: RaceWithSOFResultDTO): void {
|
||||
this.viewModel = {
|
||||
id: raceId,
|
||||
leagueId,
|
||||
scheduledAt: scheduledAt.toISOString(),
|
||||
track,
|
||||
trackId,
|
||||
car,
|
||||
carId,
|
||||
sessionType,
|
||||
status,
|
||||
strengthOfField,
|
||||
registeredCount,
|
||||
maxParticipants,
|
||||
participantCount,
|
||||
id: dto.raceId,
|
||||
leagueId: dto.leagueId,
|
||||
scheduledAt: dto.scheduledAt.toISOString(),
|
||||
track: dto.track,
|
||||
trackId: dto.trackId,
|
||||
car: dto.car,
|
||||
carId: dto.carId,
|
||||
sessionType: dto.sessionType,
|
||||
status: dto.status,
|
||||
strengthOfField: dto.strengthOfField,
|
||||
registeredCount: dto.registeredCount,
|
||||
maxParticipants: dto.maxParticipants,
|
||||
participantCount: dto.participantCount,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RaceWithSOFViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): RaceWithSOFViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
return this.viewModel;
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,18 @@ import type {
|
||||
IRacesPagePresenter,
|
||||
RacesPageViewModel,
|
||||
RaceListItemViewModel,
|
||||
RacesPageResultDTO,
|
||||
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
|
||||
|
||||
interface RacesPageInput {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string | Date;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
strengthOfField: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
}
|
||||
|
||||
export class RacesPagePresenter implements IRacesPagePresenter {
|
||||
private viewModel: RacesPageViewModel | null = null;
|
||||
|
||||
present(races: RacesPageInput[]): void {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: RacesPageResultDTO): void {
|
||||
const { races } = input;
|
||||
const now = new Date();
|
||||
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import type { ISponsorDashboardPresenter } from '@gridpilot/racing/application/presenters/ISponsorDashboardPresenter';
|
||||
import type {
|
||||
ISponsorDashboardPresenter,
|
||||
SponsorDashboardViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ISponsorDashboardPresenter';
|
||||
import type { SponsorDashboardDTO } from '@gridpilot/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
|
||||
export class SponsorDashboardPresenter implements ISponsorDashboardPresenter {
|
||||
private data: SponsorDashboardDTO | null = null;
|
||||
private viewModel: SponsorDashboardViewModel = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(data: SponsorDashboardDTO | null): void {
|
||||
this.data = data;
|
||||
this.viewModel = data;
|
||||
}
|
||||
|
||||
getViewModel(): SponsorDashboardViewModel {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getData(): SponsorDashboardDTO | null {
|
||||
return this.data;
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
import type { ISponsorSponsorshipsPresenter } from '@gridpilot/racing/application/presenters/ISponsorSponsorshipsPresenter';
|
||||
import type {
|
||||
ISponsorSponsorshipsPresenter,
|
||||
SponsorSponsorshipsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ISponsorSponsorshipsPresenter';
|
||||
import type { SponsorSponsorshipsDTO } from '@gridpilot/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
|
||||
export class SponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter {
|
||||
private data: SponsorSponsorshipsDTO | null = null;
|
||||
private viewModel: SponsorSponsorshipsViewModel = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(data: SponsorSponsorshipsDTO | null): void {
|
||||
this.data = data;
|
||||
this.viewModel = data;
|
||||
}
|
||||
|
||||
getViewModel(): SponsorSponsorshipsViewModel {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
getData(): SponsorSponsorshipsDTO | null {
|
||||
return this.data;
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { Team } from '@gridpilot/racing/domain/entities/Team';
|
||||
import type { TeamMembership } from '@gridpilot/racing/domain/types/TeamMembership';
|
||||
import type {
|
||||
ITeamDetailsPresenter,
|
||||
TeamDetailsViewModel,
|
||||
TeamDetailsResultDTO,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
|
||||
|
||||
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
private viewModel: TeamDetailsViewModel | null = null;
|
||||
|
||||
present(
|
||||
team: Team,
|
||||
membership: TeamMembership | null,
|
||||
driverId: string
|
||||
): TeamDetailsViewModel {
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(input: TeamDetailsResultDTO): void {
|
||||
const { team, membership } = input;
|
||||
const canManage = membership?.role === 'owner' || membership?.role === 'manager';
|
||||
|
||||
const viewModel: TeamDetailsViewModel = {
|
||||
@@ -23,6 +23,7 @@ export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: team.leagues,
|
||||
createdAt: team.createdAt.toISOString(),
|
||||
},
|
||||
membership: membership
|
||||
? {
|
||||
@@ -35,14 +36,9 @@ export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
};
|
||||
|
||||
this.viewModel = viewModel;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
getViewModel(): TeamDetailsViewModel {
|
||||
if (!this.viewModel) {
|
||||
throw new Error('Presenter has not been called yet');
|
||||
}
|
||||
getViewModel(): TeamDetailsViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
@@ -25,11 +15,19 @@
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/lib/*": ["./lib/*"],
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/app/*": ["./app/*"],
|
||||
"@gridpilot/identity": ["../../packages/identity/index.ts"],
|
||||
"@gridpilot/identity/*": ["../../packages/identity/*"],
|
||||
"@gridpilot/racing": ["../../packages/racing/index.ts"],
|
||||
"@gridpilot/racing/*": ["../../packages/racing/*"],
|
||||
"@gridpilot/social": ["../../packages/social/index.ts"],
|
||||
"@gridpilot/social/*": ["../../packages/social/*"],
|
||||
"@gridpilot/testing-support": ["../../packages/testing-support"],
|
||||
"@gridpilot/media": ["../../packages/media"],
|
||||
"@gridpilot/testing-support": ["../../packages/testing-support/index.ts"],
|
||||
"@gridpilot/testing-support/*": ["../../packages/testing-support/*"],
|
||||
"@gridpilot/media": ["../../packages/media/index.ts"],
|
||||
"@gridpilot/media/*": ["../../packages/media/*"],
|
||||
"@gridpilot/shared": ["../../packages/shared/index.ts"],
|
||||
"@gridpilot/shared/application": ["../../packages/shared/application"],
|
||||
"@gridpilot/shared/application/*": ["../../packages/shared/application/*"],
|
||||
@@ -40,5 +38,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", ".next"]
|
||||
}
|
||||
4
apps/website/types/third-party-shims.d.ts
vendored
Normal file
4
apps/website/types/third-party-shims.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Intentionally left blank.
|
||||
* Third-party shims are now defined in env.d.ts.
|
||||
*/
|
||||
@@ -2,6 +2,7 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
|
||||
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
|
||||
|
||||
@@ -36,6 +37,34 @@ export class AutomationEngineAdapter implements AutomationEnginePort {
|
||||
private readonly sessionRepository: SessionRepositoryPort
|
||||
) {}
|
||||
|
||||
private toStepConfig(config: HostedSessionConfig): Record<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> {
|
||||
if (!config.sessionName || config.sessionName.trim() === '') {
|
||||
return { isValid: false, error: 'Session name is required' };
|
||||
@@ -89,7 +118,7 @@ export class AutomationEngineAdapter implements AutomationEnginePort {
|
||||
|
||||
// Execute current step using the browser automation
|
||||
if (this.browserAutomation.executeStep) {
|
||||
const result = await this.browserAutomation.executeStep(currentStep, config as unknown as Record<string, unknown>);
|
||||
const result = await this.browserAutomation.executeStep(currentStep, this.toStepConfig(config));
|
||||
if (!result.success) {
|
||||
const stepDescription = StepTransitionValidator.getStepDescription(currentStep);
|
||||
const errorMessage = `Step ${currentStep.value} (${stepDescription}) failed: ${result.error}`;
|
||||
@@ -117,7 +146,7 @@ export class AutomationEngineAdapter implements AutomationEnginePort {
|
||||
if (nextStep.isFinalStep()) {
|
||||
// Execute final step handler
|
||||
if (this.browserAutomation.executeStep) {
|
||||
const result = await this.browserAutomation.executeStep(nextStep, config as unknown as Record<string, unknown>);
|
||||
const result = await this.browserAutomation.executeStep(nextStep, this.toStepConfig(config));
|
||||
if (!result.success) {
|
||||
const stepDescription = StepTransitionValidator.getStepDescription(nextStep);
|
||||
const errorMessage = `Step ${nextStep.value} (${stepDescription}) failed: ${result.error}`;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AutomationEnginePort } from '../../../../application/ports/Automat
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
|
||||
import type { HostedSessionConfig } from '../../../../domain/types/HostedSessionConfig';
|
||||
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
|
||||
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
|
||||
|
||||
@@ -19,6 +20,34 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort {
|
||||
private readonly sessionRepository: SessionRepositoryPort
|
||||
) {}
|
||||
|
||||
private toStepConfig(config: HostedSessionConfig): Record<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> {
|
||||
if (!config.sessionName || config.sessionName.trim() === '') {
|
||||
return { isValid: false, error: 'Session name is required' };
|
||||
@@ -74,7 +103,7 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort {
|
||||
if (this.browserAutomation.executeStep) {
|
||||
const result = await this.browserAutomation.executeStep(
|
||||
currentStep,
|
||||
config as unknown as Record<string, unknown>,
|
||||
this.toStepConfig(config),
|
||||
);
|
||||
if (!result.success) {
|
||||
const stepDescription = StepTransitionValidator.getStepDescription(currentStep);
|
||||
@@ -105,7 +134,7 @@ export class MockAutomationEngineAdapter implements AutomationEnginePort {
|
||||
if (this.browserAutomation.executeStep) {
|
||||
const result = await this.browserAutomation.executeStep(
|
||||
nextStep,
|
||||
config as unknown as Record<string, unknown>,
|
||||
this.toStepConfig(config),
|
||||
);
|
||||
if (!result.success) {
|
||||
const stepDescription = StepTransitionValidator.getStepDescription(nextStep);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "..",
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -3,13 +3,14 @@ import type { Season } from '../../domain/entities/Season';
|
||||
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
||||
import type { Game } from '../../domain/entities/Game';
|
||||
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface LeagueSummaryViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
createdAt: Date;
|
||||
createdAt: string;
|
||||
maxDrivers: number;
|
||||
usedDriverSlots: number;
|
||||
maxTeams?: number;
|
||||
@@ -20,7 +21,7 @@ export interface LeagueSummaryViewModel {
|
||||
scoring?: {
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
primaryChampionshipType: string;
|
||||
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
|
||||
scoringPresetId: string;
|
||||
scoringPresetName: string;
|
||||
dropPolicySummary: string;
|
||||
@@ -42,6 +43,5 @@ export interface LeagueEnrichedData {
|
||||
preset?: LeagueScoringPresetDTO;
|
||||
}
|
||||
|
||||
export interface IAllLeaguesWithCapacityAndScoringPresenter {
|
||||
present(enrichedLeagues: LeagueEnrichedData[]): AllLeaguesWithCapacityAndScoringViewModel;
|
||||
}
|
||||
export interface IAllLeaguesWithCapacityAndScoringPresenter
|
||||
extends Presenter<LeagueEnrichedData[], AllLeaguesWithCapacityAndScoringViewModel> {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { League } from '../../domain/entities/League';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface LeagueWithCapacityViewModel {
|
||||
id: string;
|
||||
@@ -24,9 +25,10 @@ export interface AllLeaguesWithCapacityViewModel {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface IAllLeaguesWithCapacityPresenter {
|
||||
present(
|
||||
leagues: League[],
|
||||
memberCounts: Map<string, number>
|
||||
): AllLeaguesWithCapacityViewModel;
|
||||
}
|
||||
export interface AllLeaguesWithCapacityResultDTO {
|
||||
leagues: League[];
|
||||
memberCounts: Map<string, number>;
|
||||
}
|
||||
|
||||
export interface IAllLeaguesWithCapacityPresenter
|
||||
extends Presenter<AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel> {}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
|
||||
|
||||
export interface AllRacesListItemViewModel {
|
||||
@@ -21,7 +23,7 @@ export interface AllRacesPageViewModel {
|
||||
filters: AllRacesFilterOptionsViewModel;
|
||||
}
|
||||
|
||||
export interface IAllRacesPagePresenter {
|
||||
present(viewModel: AllRacesPageViewModel): void;
|
||||
getViewModel(): AllRacesPageViewModel | null;
|
||||
}
|
||||
export type AllRacesPageResultDTO = AllRacesPageViewModel;
|
||||
|
||||
export interface IAllRacesPagePresenter
|
||||
extends Presenter<AllRacesPageResultDTO, AllRacesPageViewModel> {}
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface TeamListItemViewModel {
|
||||
@@ -19,7 +18,16 @@ export interface AllTeamsViewModel {
|
||||
}
|
||||
|
||||
export interface AllTeamsResultDTO {
|
||||
teams: Array<Team & { memberCount: number }>;
|
||||
teams: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt: Date;
|
||||
memberCount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IAllTeamsPresenter
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface DashboardDriverSummaryViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -82,7 +84,7 @@ export interface DashboardOverviewViewModel {
|
||||
friends: DashboardFriendSummaryViewModel[];
|
||||
}
|
||||
|
||||
export interface IDashboardOverviewPresenter {
|
||||
present(viewModel: DashboardOverviewViewModel): void;
|
||||
getViewModel(): DashboardOverviewViewModel | null;
|
||||
}
|
||||
export type DashboardOverviewResultDTO = DashboardOverviewViewModel;
|
||||
|
||||
export interface IDashboardOverviewPresenter
|
||||
extends Presenter<DashboardOverviewResultDTO, DashboardOverviewViewModel> {}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Driver } from '../../domain/entities/Driver';
|
||||
import type { SkillLevel } from '../../domain/services/SkillLevelService';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export type { SkillLevel };
|
||||
|
||||
@@ -24,13 +25,21 @@ export interface DriversLeaderboardViewModel {
|
||||
activeCount: number;
|
||||
}
|
||||
|
||||
export interface IDriversLeaderboardPresenter {
|
||||
present(
|
||||
drivers: Driver[],
|
||||
rankings: Array<{ driverId: string; rating: number; overallRank: number }>,
|
||||
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
|
||||
avatarUrls: Record<string, string>
|
||||
): DriversLeaderboardViewModel;
|
||||
export interface DriversLeaderboardResultDTO {
|
||||
drivers: Driver[];
|
||||
rankings: Array<{ driverId: string; rating: number; overallRank: number | null }>;
|
||||
stats: Record<
|
||||
string,
|
||||
{
|
||||
rating: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
totalRaces: number;
|
||||
overallRank: number | null;
|
||||
}
|
||||
>;
|
||||
avatarUrls: Record<string, string>;
|
||||
}
|
||||
|
||||
getViewModel(): DriversLeaderboardViewModel;
|
||||
}
|
||||
export interface IDriversLeaderboardPresenter
|
||||
extends Presenter<DriversLeaderboardResultDTO, DriversLeaderboardViewModel> {}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface LeagueDriverSeasonStatsItemViewModel {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
@@ -24,18 +26,18 @@ export interface LeagueDriverSeasonStatsViewModel {
|
||||
stats: LeagueDriverSeasonStatsItemViewModel[];
|
||||
}
|
||||
|
||||
export interface ILeagueDriverSeasonStatsPresenter {
|
||||
present(
|
||||
leagueId: string,
|
||||
standings: Array<{
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
racesCompleted: number;
|
||||
}>,
|
||||
penalties: Map<string, { baseDelta: number; bonusDelta: number }>,
|
||||
driverResults: Map<string, Array<{ position: number }>>,
|
||||
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
|
||||
): LeagueDriverSeasonStatsViewModel;
|
||||
getViewModel(): LeagueDriverSeasonStatsViewModel;
|
||||
}
|
||||
export interface LeagueDriverSeasonStatsResultDTO {
|
||||
leagueId: string;
|
||||
standings: Array<{
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
racesCompleted: number;
|
||||
}>;
|
||||
penalties: Map<string, { baseDelta: number; bonusDelta: number }>;
|
||||
driverResults: Map<string, Array<{ position: number }>>;
|
||||
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>;
|
||||
}
|
||||
|
||||
export interface ILeagueDriverSeasonStatsPresenter
|
||||
extends Presenter<LeagueDriverSeasonStatsResultDTO, LeagueDriverSeasonStatsViewModel> {}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig';
|
||||
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface LeagueScoringChampionshipViewModel {
|
||||
id: string;
|
||||
@@ -32,7 +33,5 @@ export interface LeagueScoringConfigData {
|
||||
championships: ChampionshipConfig[];
|
||||
}
|
||||
|
||||
export interface ILeagueScoringConfigPresenter {
|
||||
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel;
|
||||
getViewModel(): LeagueScoringConfigViewModel;
|
||||
}
|
||||
export interface ILeagueScoringConfigPresenter
|
||||
extends Presenter<LeagueScoringConfigData, LeagueScoringConfigViewModel> {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SessionType, RaceStatus } from '../../domain/entities/Race';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface RaceDetailEntryViewModel {
|
||||
id: string;
|
||||
@@ -55,7 +56,5 @@ export interface RaceDetailViewModel {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IRaceDetailPresenter {
|
||||
present(viewModel: RaceDetailViewModel): RaceDetailViewModel;
|
||||
getViewModel(): RaceDetailViewModel | null;
|
||||
}
|
||||
export interface IRaceDetailPresenter
|
||||
extends Presenter<RaceDetailViewModel, RaceDetailViewModel> {}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty';
|
||||
import type { Penalty, PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface RacePenaltyViewModel {
|
||||
@@ -24,21 +24,7 @@ export interface RacePenaltiesViewModel {
|
||||
}
|
||||
|
||||
export interface RacePenaltiesResultDTO {
|
||||
penalties: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
type: PenaltyType;
|
||||
value?: number;
|
||||
reason: string;
|
||||
protestId?: string;
|
||||
issuedBy: string;
|
||||
status: PenaltyStatus;
|
||||
issuedAt: Date;
|
||||
appliedAt?: Date;
|
||||
notes?: string;
|
||||
getDescription(): string;
|
||||
}>;
|
||||
penalties: Penalty[];
|
||||
driverMap: Map<string, string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProtestStatus, ProtestIncident } from '../../domain/entities/Protest';
|
||||
import type { Protest, ProtestStatus, ProtestIncident } from '../../domain/entities/Protest';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface RaceProtestViewModel {
|
||||
@@ -24,20 +24,7 @@ export interface RaceProtestsViewModel {
|
||||
}
|
||||
|
||||
export interface RaceProtestsResultDTO {
|
||||
protests: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
incident: ProtestIncident;
|
||||
comment?: string;
|
||||
proofVideoUrl?: string;
|
||||
status: ProtestStatus;
|
||||
reviewedBy?: string;
|
||||
decisionNotes?: string;
|
||||
filedAt: Date;
|
||||
reviewedAt?: Date;
|
||||
}>;
|
||||
protests: Protest[];
|
||||
driverMap: Map<string, string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface RaceRegistrationsViewModel {
|
||||
registeredDriverIds: string[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface IRaceRegistrationsPresenter {
|
||||
present(registeredDriverIds: string[]): RaceRegistrationsViewModel;
|
||||
getViewModel(): RaceRegistrationsViewModel;
|
||||
}
|
||||
export interface RaceRegistrationsResultDTO {
|
||||
registeredDriverIds: string[];
|
||||
}
|
||||
|
||||
export interface IRaceRegistrationsPresenter
|
||||
extends Presenter<RaceRegistrationsResultDTO, RaceRegistrationsViewModel> {}
|
||||
@@ -2,6 +2,7 @@ import type { RaceStatus } from '../../domain/entities/Race';
|
||||
import type { Result } from '../../domain/entities/Result';
|
||||
import type { Driver } from '../../domain/entities/Driver';
|
||||
import type { PenaltyType } from '../../domain/entities/Penalty';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface RaceResultsHeaderViewModel {
|
||||
id: string;
|
||||
@@ -28,13 +29,11 @@ export interface RaceResultsDetailViewModel {
|
||||
results: Result[];
|
||||
drivers: Driver[];
|
||||
penalties: RaceResultsPenaltySummaryViewModel[];
|
||||
pointsSystem: Record<number, number>;
|
||||
pointsSystem?: Record<number, number>;
|
||||
fastestLapTime?: number;
|
||||
currentDriverId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IRaceResultsDetailPresenter {
|
||||
present(viewModel: RaceResultsDetailViewModel): RaceResultsDetailViewModel;
|
||||
getViewModel(): RaceResultsDetailViewModel | null;
|
||||
}
|
||||
export interface IRaceResultsDetailPresenter
|
||||
extends Presenter<RaceResultsDetailViewModel, RaceResultsDetailViewModel> {}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface RaceWithSOFViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
@@ -14,21 +16,21 @@ export interface RaceWithSOFViewModel {
|
||||
participantCount: number;
|
||||
}
|
||||
|
||||
export interface IRaceWithSOFPresenter {
|
||||
present(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
scheduledAt: Date,
|
||||
track: string,
|
||||
trackId: string,
|
||||
car: string,
|
||||
carId: string,
|
||||
sessionType: string,
|
||||
status: string,
|
||||
strengthOfField: number | null,
|
||||
registeredCount: number,
|
||||
maxParticipants: number,
|
||||
participantCount: number
|
||||
): RaceWithSOFViewModel;
|
||||
getViewModel(): RaceWithSOFViewModel;
|
||||
}
|
||||
export interface RaceWithSOFResultDTO {
|
||||
raceId: string;
|
||||
leagueId: string;
|
||||
scheduledAt: Date;
|
||||
track: string;
|
||||
trackId: string;
|
||||
car: string;
|
||||
carId: string;
|
||||
sessionType: string;
|
||||
status: string;
|
||||
strengthOfField: number | null;
|
||||
registeredCount: number;
|
||||
maxParticipants: number;
|
||||
participantCount: number;
|
||||
}
|
||||
|
||||
export interface IRaceWithSOFPresenter
|
||||
extends Presenter<RaceWithSOFResultDTO, RaceWithSOFViewModel> {}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface RaceListItemViewModel {
|
||||
id: string;
|
||||
track: string;
|
||||
@@ -25,7 +27,9 @@ export interface RacesPageViewModel {
|
||||
recentResults: RaceListItemViewModel[];
|
||||
}
|
||||
|
||||
export interface IRacesPagePresenter {
|
||||
present(races: any[]): void;
|
||||
getViewModel(): RacesPageViewModel;
|
||||
}
|
||||
export interface RacesPageResultDTO {
|
||||
races: any[];
|
||||
}
|
||||
|
||||
export interface IRacesPagePresenter
|
||||
extends Presenter<RacesPageResultDTO, RacesPageViewModel> {}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { SponsoredLeagueDTO, SponsorDashboardDTO } from '../use-cases/GetSponsorDashboardUseCase';
|
||||
import type { SponsorDashboardDTO } from '../use-cases/GetSponsorDashboardUseCase';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface ISponsorDashboardPresenter {
|
||||
present(data: SponsorDashboardDTO | null): void;
|
||||
}
|
||||
export type SponsorDashboardViewModel = SponsorDashboardDTO | null;
|
||||
|
||||
export interface ISponsorDashboardPresenter
|
||||
extends Presenter<SponsorDashboardDTO | null, SponsorDashboardViewModel> {}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { SponsorSponsorshipsDTO } from '../use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface ISponsorSponsorshipsPresenter {
|
||||
present(data: SponsorSponsorshipsDTO | null): void;
|
||||
}
|
||||
export type SponsorSponsorshipsViewModel = SponsorSponsorshipsDTO | null;
|
||||
|
||||
export interface ISponsorSponsorshipsPresenter
|
||||
extends Presenter<SponsorSponsorshipsDTO | null, SponsorSponsorshipsViewModel> {}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type { TeamMembership } from '../../domain/types/TeamMembership';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface TeamDetailsViewModel {
|
||||
team: {
|
||||
@@ -9,9 +10,7 @@ export interface TeamDetailsViewModel {
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
specialization?: 'endurance' | 'sprint' | 'mixed';
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
createdAt: string;
|
||||
};
|
||||
membership: {
|
||||
role: 'owner' | 'manager' | 'member';
|
||||
@@ -21,10 +20,11 @@ export interface TeamDetailsViewModel {
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export interface ITeamDetailsPresenter {
|
||||
present(
|
||||
team: Team,
|
||||
membership: TeamMembership | null,
|
||||
driverId: string
|
||||
): TeamDetailsViewModel;
|
||||
}
|
||||
export interface TeamDetailsResultDTO {
|
||||
team: Team;
|
||||
membership: TeamMembership | null;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface ITeamDetailsPresenter
|
||||
extends Presenter<TeamDetailsResultDTO, TeamDetailsViewModel> {}
|
||||
@@ -27,15 +27,14 @@ export class CreateTeamUseCase {
|
||||
throw new Error('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const team: Team = {
|
||||
const team = Team.create({
|
||||
id: `team-${Date.now()}`,
|
||||
name,
|
||||
tag,
|
||||
description,
|
||||
ownerId,
|
||||
leagues,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
const createdTeam = await this.teamRepository.create(team);
|
||||
|
||||
|
||||
@@ -4,15 +4,25 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
AllLeaguesWithCapacityAndScoringViewModel,
|
||||
IAllLeaguesWithCapacityAndScoringPresenter,
|
||||
LeagueEnrichedData,
|
||||
} from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all leagues with capacity and scoring information.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
implements AsyncUseCase<void, void>
|
||||
implements
|
||||
UseCase<
|
||||
void,
|
||||
LeagueEnrichedData[],
|
||||
AllLeaguesWithCapacityAndScoringViewModel,
|
||||
IAllLeaguesWithCapacityAndScoringPresenter
|
||||
>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
@@ -21,10 +31,14 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
private readonly presetProvider: LeagueScoringPresetProvider,
|
||||
public readonly presenter: IAllLeaguesWithCapacityAndScoringPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
async execute(
|
||||
_input: void,
|
||||
presenter: IAllLeaguesWithCapacityAndScoringPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
|
||||
const enrichedLeagues: LeagueEnrichedData[] = [];
|
||||
@@ -42,18 +56,22 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
).length;
|
||||
|
||||
const seasons = await this.seasonRepository.findByLeagueId(league.id);
|
||||
const activeSeason = seasons && seasons.length > 0
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: undefined;
|
||||
const activeSeason =
|
||||
seasons && seasons.length > 0
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: undefined;
|
||||
|
||||
let scoringConfig: LeagueEnrichedData['scoringConfig'];
|
||||
let game: LeagueEnrichedData['game'];
|
||||
let preset: LeagueEnrichedData['preset'];
|
||||
|
||||
if (activeSeason) {
|
||||
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
const scoringConfigResult =
|
||||
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
scoringConfig = scoringConfigResult ?? undefined;
|
||||
if (scoringConfig) {
|
||||
game = await this.gameRepository.findById(activeSeason.gameId);
|
||||
const gameResult = await this.gameRepository.findById(activeSeason.gameId);
|
||||
game = gameResult ?? undefined;
|
||||
const presetId = scoringConfig.scoringPresetId;
|
||||
if (presetId) {
|
||||
preset = this.presetProvider.getPresetById(presetId);
|
||||
@@ -64,13 +82,13 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
enrichedLeagues.push({
|
||||
league,
|
||||
usedDriverSlots,
|
||||
season: activeSeason,
|
||||
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
|
||||
...(game ?? undefined ? { game } : {}),
|
||||
...(preset ?? undefined ? { preset } : {}),
|
||||
...(activeSeason ? { season: activeSeason } : {}),
|
||||
...(scoringConfig ? { scoringConfig } : {}),
|
||||
...(game ? { game } : {}),
|
||||
...(preset ? { preset } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
this.presenter.present(enrichedLeagues);
|
||||
presenter.present(enrichedLeagues);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,30 @@
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IAllLeaguesWithCapacityPresenter } from '../presenters/IAllLeaguesWithCapacityPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IAllLeaguesWithCapacityPresenter,
|
||||
AllLeaguesWithCapacityResultDTO,
|
||||
AllLeaguesWithCapacityViewModel,
|
||||
} from '../presenters/IAllLeaguesWithCapacityPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all leagues with capacity information.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllLeaguesWithCapacityUseCase
|
||||
implements AsyncUseCase<void, void>
|
||||
implements UseCase<void, AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel, IAllLeaguesWithCapacityPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
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 memberCounts = new Map<string, number>();
|
||||
@@ -36,6 +44,11 @@ export class GetAllLeaguesWithCapacityUseCase
|
||||
memberCounts.set(league.id, usedSlots);
|
||||
}
|
||||
|
||||
this.presenter.present(leagues, memberCounts);
|
||||
const dto: AllLeaguesWithCapacityResultDTO = {
|
||||
leagues,
|
||||
memberCounts,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,21 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type {
|
||||
IAllRacesPagePresenter,
|
||||
AllRacesPageResultDTO,
|
||||
AllRacesPageViewModel,
|
||||
AllRacesListItemViewModel,
|
||||
AllRacesFilterOptionsViewModel,
|
||||
} from '../presenters/IAllRacesPagePresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
|
||||
export class GetAllRacesPageDataUseCase
|
||||
implements AsyncUseCase<void, void> {
|
||||
implements UseCase<void, AllRacesPageResultDTO, AllRacesPageViewModel, IAllRacesPagePresenter> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
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([
|
||||
this.raceRepository.findAll(),
|
||||
this.leagueRepository.findAll(),
|
||||
@@ -59,6 +59,7 @@ export class GetAllRacesPageDataUseCase
|
||||
filters,
|
||||
};
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
presenter.reset();
|
||||
presenter.present(viewModel);
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,17 @@ export class GetAllTeamsUseCase
|
||||
|
||||
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) => {
|
||||
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
|
||||
return {
|
||||
...team,
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: [...team.leagues],
|
||||
createdAt: team.createdAt,
|
||||
memberCount,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IDashboardOverviewPresenter,
|
||||
DashboardOverviewViewModel,
|
||||
@@ -34,8 +33,7 @@ export interface GetDashboardOverviewParams {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class GetDashboardOverviewUseCase
|
||||
implements AsyncUseCase<GetDashboardOverviewParams, void> {
|
||||
export class GetDashboardOverviewUseCase {
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
@@ -48,10 +46,9 @@ export class GetDashboardOverviewUseCase
|
||||
private readonly socialRepository: ISocialGraphRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null,
|
||||
public readonly presenter: IDashboardOverviewPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetDashboardOverviewParams): Promise<void> {
|
||||
async execute(params: GetDashboardOverviewParams, presenter: IDashboardOverviewPresenter): Promise<void> {
|
||||
const { driverId } = params;
|
||||
|
||||
const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([
|
||||
@@ -137,7 +134,8 @@ export class GetDashboardOverviewUseCase
|
||||
friends: friendsSummary,
|
||||
};
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
presenter.reset();
|
||||
presenter.present(viewModel);
|
||||
}
|
||||
|
||||
private async getDriverLeagues(allLeagues: any[], driverId: string): Promise<any[]> {
|
||||
|
||||
@@ -2,30 +2,36 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
|
||||
import type { IRankingService } from '../../domain/services/IRankingService';
|
||||
import type { IDriverStatsService } from '../../domain/services/IDriverStatsService';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { IDriversLeaderboardPresenter } from '../presenters/IDriversLeaderboardPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IDriversLeaderboardPresenter,
|
||||
DriversLeaderboardResultDTO,
|
||||
DriversLeaderboardViewModel,
|
||||
} from '../presenters/IDriversLeaderboardPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving driver leaderboard data.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetDriversLeaderboardUseCase
|
||||
implements AsyncUseCase<void, void> {
|
||||
implements UseCase<void, DriversLeaderboardResultDTO, DriversLeaderboardViewModel, IDriversLeaderboardPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly rankingService: IRankingService,
|
||||
private readonly driverStatsService: IDriverStatsService,
|
||||
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 rankings = this.rankingService.getAllDriverRankings();
|
||||
|
||||
const stats: Record<string, any> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
|
||||
const stats: DriversLeaderboardResultDTO['stats'] = {};
|
||||
const avatarUrls: DriversLeaderboardResultDTO['avatarUrls'] = {};
|
||||
|
||||
for (const driver of drivers) {
|
||||
const driverStats = this.driverStatsService.getDriverStats(driver.id);
|
||||
if (driverStats) {
|
||||
@@ -33,7 +39,14 @@ export class GetDriversLeaderboardUseCase
|
||||
}
|
||||
avatarUrls[driver.id] = this.imageService.getDriverAvatar(driver.id);
|
||||
}
|
||||
|
||||
this.presenter.present(drivers, rankings, stats, avatarUrls);
|
||||
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers,
|
||||
rankings,
|
||||
stats,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
ILeagueDriverSeasonStatsPresenter,
|
||||
LeagueDriverSeasonStatsResultDTO,
|
||||
LeagueDriverSeasonStatsViewModel,
|
||||
} from '../presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface DriverRatingPort {
|
||||
getRating(driverId: string): { rating: number | null; ratingChange: number | null };
|
||||
@@ -18,17 +22,27 @@ export interface GetLeagueDriverSeasonStatsUseCaseParams {
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueDriverSeasonStatsUseCase
|
||||
implements AsyncUseCase<GetLeagueDriverSeasonStatsUseCaseParams, void> {
|
||||
implements
|
||||
UseCase<
|
||||
GetLeagueDriverSeasonStatsUseCaseParams,
|
||||
LeagueDriverSeasonStatsResultDTO,
|
||||
LeagueDriverSeasonStatsViewModel,
|
||||
ILeagueDriverSeasonStatsPresenter
|
||||
>
|
||||
{
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly driverRatingPort: DriverRatingPort,
|
||||
public readonly presenter: ILeagueDriverSeasonStatsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetLeagueDriverSeasonStatsUseCaseParams): Promise<void> {
|
||||
async execute(
|
||||
params: GetLeagueDriverSeasonStatsUseCaseParams,
|
||||
presenter: ILeagueDriverSeasonStatsPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
const { leagueId } = params;
|
||||
|
||||
// Get standings and races for the league
|
||||
@@ -70,16 +84,26 @@ export class GetLeagueDriverSeasonStatsUseCase
|
||||
// Collect driver results
|
||||
const driverResults = new Map<string, Array<{ position: number }>>();
|
||||
for (const standing of standings) {
|
||||
const results = await this.resultRepository.findByDriverIdAndLeagueId(standing.driverId, leagueId);
|
||||
const results = await this.resultRepository.findByDriverIdAndLeagueId(
|
||||
standing.driverId,
|
||||
leagueId,
|
||||
);
|
||||
driverResults.set(standing.driverId, results);
|
||||
}
|
||||
|
||||
this.presenter.present(
|
||||
const dto: LeagueDriverSeasonStatsResultDTO = {
|
||||
leagueId,
|
||||
standings,
|
||||
penaltiesByDriver,
|
||||
standings: standings.map(standing => ({
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
points: standing.points,
|
||||
racesCompleted: standing.racesCompleted,
|
||||
})),
|
||||
penalties: penaltiesByDriver,
|
||||
driverResults,
|
||||
driverRatings
|
||||
);
|
||||
driverRatings,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,9 @@ export class GetLeagueFullConfigUseCase
|
||||
|
||||
const data: LeagueFullConfigData = {
|
||||
league,
|
||||
activeSeason,
|
||||
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
|
||||
...(game ?? undefined ? { game } : {}),
|
||||
...(activeSeason ? { activeSeason } : {}),
|
||||
...(scoringConfig ? { scoringConfig } : {}),
|
||||
...(game ? { game } : {}),
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
|
||||
@@ -3,25 +3,29 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
ILeagueScoringConfigPresenter,
|
||||
LeagueScoringConfigData,
|
||||
LeagueScoringConfigViewModel,
|
||||
} from '../presenters/ILeagueScoringConfigPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a league's scoring configuration for its active season.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueScoringConfigUseCase
|
||||
implements AsyncUseCase<{ leagueId: string }, void> {
|
||||
implements UseCase<{ leagueId: string }, LeagueScoringConfigData, LeagueScoringConfigViewModel, ILeagueScoringConfigPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
private readonly presetProvider: LeagueScoringPresetProvider,
|
||||
public readonly presenter: ILeagueScoringConfigPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<void> {
|
||||
async execute(params: { leagueId: string }, presenter: ILeagueScoringConfigPresenter): Promise<void> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
@@ -65,6 +69,7 @@ export class GetLeagueScoringConfigUseCase
|
||||
championships: scoringConfig.championships,
|
||||
};
|
||||
|
||||
this.presenter.present(data);
|
||||
presenter.reset();
|
||||
presenter.present(data);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
RaceDetailEntryViewModel,
|
||||
RaceDetailUserResultViewModel,
|
||||
} from '../presenters/IRaceDetailPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case: GetRaceDetailUseCase
|
||||
@@ -30,7 +31,9 @@ export interface GetRaceDetailQueryParams {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class GetRaceDetailUseCase {
|
||||
export class GetRaceDetailUseCase
|
||||
implements UseCase<GetRaceDetailQueryParams, RaceDetailViewModel, RaceDetailViewModel, IRaceDetailPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
@@ -40,10 +43,11 @@ export class GetRaceDetailUseCase {
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
private readonly imageService: IImageServicePort,
|
||||
public readonly presenter: IRaceDetailPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceDetailQueryParams): Promise<void> {
|
||||
async execute(params: GetRaceDetailQueryParams, presenter: IRaceDetailPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { raceId, driverId } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
@@ -59,7 +63,7 @@ export class GetRaceDetailUseCase {
|
||||
userResult: null,
|
||||
error: 'Race not found',
|
||||
};
|
||||
this.presenter.present(emptyViewModel);
|
||||
presenter.present(emptyViewModel);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,8 +125,8 @@ export class GetRaceDetailUseCase {
|
||||
sessionType: race.sessionType,
|
||||
status: race.status,
|
||||
strengthOfField: race.strengthOfField ?? null,
|
||||
registeredCount: race.registeredCount,
|
||||
maxParticipants: race.maxParticipants,
|
||||
...(race.registeredCount !== undefined ? { registeredCount: race.registeredCount } : {}),
|
||||
...(race.maxParticipants !== undefined ? { maxParticipants: race.maxParticipants } : {}),
|
||||
};
|
||||
|
||||
const leagueView: RaceDetailLeagueViewModel | null = league
|
||||
@@ -131,8 +135,12 @@ export class GetRaceDetailUseCase {
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
settings: {
|
||||
maxDrivers: league.settings.maxDrivers,
|
||||
qualifyingFormat: league.settings.qualifyingFormat,
|
||||
...(league.settings.maxDrivers !== undefined
|
||||
? { maxDrivers: league.settings.maxDrivers }
|
||||
: {}),
|
||||
...(league.settings.qualifyingFormat !== undefined
|
||||
? { qualifyingFormat: league.settings.qualifyingFormat }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: null;
|
||||
@@ -148,7 +156,7 @@ export class GetRaceDetailUseCase {
|
||||
userResult: userResultView,
|
||||
};
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
presenter.present(viewModel);
|
||||
}
|
||||
|
||||
private calculateRatingChange(position: number): number {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
|
||||
import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistrationsPresenter';
|
||||
import type {
|
||||
IRaceRegistrationsPresenter,
|
||||
RaceRegistrationsResultDTO,
|
||||
RaceRegistrationsViewModel,
|
||||
} from '../presenters/IRaceRegistrationsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case: GetRaceRegistrationsUseCase
|
||||
@@ -8,15 +13,26 @@ import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistratio
|
||||
* Returns registered driver IDs for a race.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetRaceRegistrationsUseCase {
|
||||
export class GetRaceRegistrationsUseCase
|
||||
implements UseCase<GetRaceRegistrationsQueryParamsDTO, RaceRegistrationsResultDTO, RaceRegistrationsViewModel, IRaceRegistrationsPresenter>
|
||||
{
|
||||
constructor(
|
||||
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 registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId);
|
||||
this.presenter.present(registeredDriverIds);
|
||||
|
||||
const dto: RaceRegistrationsResultDTO = {
|
||||
registeredDriverIds,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
RaceResultsDetailViewModel,
|
||||
RaceResultsPenaltySummaryViewModel,
|
||||
} from '../presenters/IRaceResultsDetailPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
import type { League } from '../../domain/entities/League';
|
||||
import type { Result } from '../../domain/entities/Result';
|
||||
import type { Driver } from '../../domain/entities/Driver';
|
||||
@@ -18,8 +19,8 @@ export interface GetRaceResultsDetailParams {
|
||||
driverId?: string;
|
||||
}
|
||||
|
||||
function buildPointsSystem(league: League | null): Record<number, number> {
|
||||
if (!league) return {};
|
||||
function buildPointsSystem(league: League | null): Record<number, number> | undefined {
|
||||
if (!league) return undefined;
|
||||
|
||||
const pointsSystems: Record<string, Record<number, number>> = {
|
||||
'f1-2024': {
|
||||
@@ -53,11 +54,17 @@ function buildPointsSystem(league: League | null): Record<number, number> {
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
league.settings.customPoints ||
|
||||
pointsSystems[league.settings.pointsSystem] ||
|
||||
pointsSystems['f1-2024']
|
||||
);
|
||||
const customPoints = league.settings.customPoints;
|
||||
if (customPoints) {
|
||||
return customPoints;
|
||||
}
|
||||
|
||||
const preset = pointsSystems[league.settings.pointsSystem];
|
||||
if (preset) {
|
||||
return preset;
|
||||
}
|
||||
|
||||
return pointsSystems['f1-2024'];
|
||||
}
|
||||
|
||||
function getFastestLapTime(results: Result[]): number | undefined {
|
||||
@@ -73,17 +80,28 @@ function mapPenaltySummary(penalties: Penalty[]): RaceResultsPenaltySummaryViewM
|
||||
}));
|
||||
}
|
||||
|
||||
export class GetRaceResultsDetailUseCase {
|
||||
export class GetRaceResultsDetailUseCase
|
||||
implements
|
||||
UseCase<
|
||||
GetRaceResultsDetailParams,
|
||||
RaceResultsDetailViewModel,
|
||||
RaceResultsDetailViewModel,
|
||||
IRaceResultsDetailPresenter
|
||||
>
|
||||
{
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
public readonly presenter: IRaceResultsDetailPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceResultsDetailParams): Promise<void> {
|
||||
async execute(
|
||||
params: GetRaceResultsDetailParams,
|
||||
presenter: IRaceResultsDetailPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
const { raceId, driverId } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
@@ -95,11 +113,10 @@ export class GetRaceResultsDetailUseCase {
|
||||
results: [],
|
||||
drivers: [],
|
||||
penalties: [],
|
||||
pointsSystem: {},
|
||||
currentDriverId: driverId,
|
||||
...(driverId ? { currentDriverId: driverId } : {}),
|
||||
error: 'Race not found',
|
||||
};
|
||||
this.presenter.present(errorViewModel);
|
||||
presenter.present(errorViewModel);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,12 +128,12 @@ export class GetRaceResultsDetailUseCase {
|
||||
]);
|
||||
|
||||
const effectiveCurrentDriverId =
|
||||
driverId || (drivers.length > 0 ? drivers[0]!.id : undefined);
|
||||
driverId ?? (drivers.length > 0 ? drivers[0]!.id : undefined);
|
||||
|
||||
const pointsSystem = buildPointsSystem(league as League | null);
|
||||
const fastestLapTime = getFastestLapTime(results);
|
||||
const penaltySummary = mapPenaltySummary(penalties);
|
||||
|
||||
|
||||
const viewModel: RaceResultsDetailViewModel = {
|
||||
race: {
|
||||
id: race.id,
|
||||
@@ -134,11 +151,11 @@ export class GetRaceResultsDetailUseCase {
|
||||
results,
|
||||
drivers,
|
||||
penalties: penaltySummary,
|
||||
pointsSystem,
|
||||
...(pointsSystem ? { pointsSystem } : {}),
|
||||
...(fastestLapTime !== undefined ? { fastestLapTime } : {}),
|
||||
currentDriverId: effectiveCurrentDriverId,
|
||||
...(effectiveCurrentDriverId ? { currentDriverId: effectiveCurrentDriverId } : {}),
|
||||
};
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
presenter.present(viewModel);
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,16 @@ import {
|
||||
AverageStrengthOfFieldCalculator,
|
||||
type StrengthOfFieldCalculator,
|
||||
} from '../../domain/services/StrengthOfFieldCalculator';
|
||||
import type { IRaceWithSOFPresenter } from '../presenters/IRaceWithSOFPresenter';
|
||||
import type { IRaceWithSOFPresenter, RaceWithSOFResultDTO } from '../presenters/IRaceWithSOFPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetRaceWithSOFQueryParams {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class GetRaceWithSOFUseCase {
|
||||
export class GetRaceWithSOFUseCase
|
||||
implements UseCase<GetRaceWithSOFQueryParams, RaceWithSOFResultDTO, import('../presenters/IRaceWithSOFPresenter').RaceWithSOFViewModel, IRaceWithSOFPresenter>
|
||||
{
|
||||
private readonly sofCalculator: StrengthOfFieldCalculator;
|
||||
|
||||
constructor(
|
||||
@@ -28,18 +31,19 @@ export class GetRaceWithSOFUseCase {
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
public readonly presenter: IRaceWithSOFPresenter,
|
||||
sofCalculator?: StrengthOfFieldCalculator,
|
||||
) {
|
||||
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
|
||||
}
|
||||
|
||||
async execute(params: GetRaceWithSOFQueryParams): Promise<void> {
|
||||
async execute(params: GetRaceWithSOFQueryParams, presenter: IRaceWithSOFPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { raceId } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get participant IDs based on race status
|
||||
@@ -56,30 +60,34 @@ export class GetRaceWithSOFUseCase {
|
||||
|
||||
// Use stored SOF if available, otherwise calculate
|
||||
let strengthOfField = race.strengthOfField ?? null;
|
||||
|
||||
|
||||
if (strengthOfField === null && participantIds.length > 0) {
|
||||
const ratings = this.driverRatingProvider.getRatings(participantIds);
|
||||
const driverRatings = participantIds
|
||||
.filter(id => ratings.has(id))
|
||||
.map(id => ({ driverId: id, rating: ratings.get(id)! }));
|
||||
|
||||
|
||||
strengthOfField = this.sofCalculator.calculate(driverRatings);
|
||||
}
|
||||
|
||||
this.presenter.present(
|
||||
race.id,
|
||||
race.leagueId,
|
||||
race.scheduledAt,
|
||||
race.track,
|
||||
race.trackId,
|
||||
race.car,
|
||||
race.carId,
|
||||
race.sessionType,
|
||||
race.status,
|
||||
presenter.reset();
|
||||
|
||||
const dto: RaceWithSOFResultDTO = {
|
||||
raceId: race.id,
|
||||
leagueId: race.leagueId,
|
||||
scheduledAt: race.scheduledAt,
|
||||
track: race.track ?? '',
|
||||
trackId: race.trackId ?? '',
|
||||
car: race.car ?? '',
|
||||
carId: race.carId ?? '',
|
||||
sessionType: race.sessionType,
|
||||
status: race.status,
|
||||
strengthOfField,
|
||||
race.registeredCount ?? participantIds.length,
|
||||
race.maxParticipants,
|
||||
participantIds.length
|
||||
);
|
||||
registeredCount: race.registeredCount ?? participantIds.length,
|
||||
maxParticipants: race.maxParticipants ?? participantIds.length,
|
||||
participantCount: participantIds.length,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { IRacesPagePresenter } from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
|
||||
import type {
|
||||
IRacesPagePresenter,
|
||||
RacesPageResultDTO,
|
||||
RacesPageViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export class GetRacesPageDataUseCase {
|
||||
export class GetRacesPageDataUseCase
|
||||
implements UseCase<void, RacesPageResultDTO, RacesPageViewModel, IRacesPagePresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
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([
|
||||
this.raceRepository.findAll(),
|
||||
this.leagueRepository.findAll(),
|
||||
@@ -33,6 +41,10 @@ export class GetRacesPageDataUseCase {
|
||||
isPast: race.isPast(),
|
||||
}));
|
||||
|
||||
this.presenter.present(races);
|
||||
const dto: RacesPageResultDTO = {
|
||||
races,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,11 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ISponsorDashboardPresenter } from '../presenters/ISponsorDashboardPresenter';
|
||||
import type {
|
||||
ISponsorDashboardPresenter,
|
||||
SponsorDashboardViewModel,
|
||||
} from '../presenters/ISponsorDashboardPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetSponsorDashboardQueryParams {
|
||||
sponsorId: string;
|
||||
@@ -47,7 +51,9 @@ export interface SponsorDashboardDTO {
|
||||
};
|
||||
}
|
||||
|
||||
export class GetSponsorDashboardUseCase {
|
||||
export class GetSponsorDashboardUseCase
|
||||
implements UseCase<GetSponsorDashboardQueryParams, SponsorDashboardDTO | null, SponsorDashboardViewModel, ISponsorDashboardPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
|
||||
@@ -55,15 +61,19 @@ export class GetSponsorDashboardUseCase {
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly presenter: ISponsorDashboardPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetSponsorDashboardQueryParams): Promise<void> {
|
||||
async execute(
|
||||
params: GetSponsorDashboardQueryParams,
|
||||
presenter: ISponsorDashboardPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
||||
if (!sponsor) {
|
||||
this.presenter.present(null);
|
||||
presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,11 +149,11 @@ export class GetSponsorDashboardUseCase {
|
||||
|
||||
// Calculate exposure score (0-100 based on tier distribution)
|
||||
const mainSponsorships = sponsorships.filter(s => s.tier === 'main').length;
|
||||
const exposure = sponsorships.length > 0
|
||||
const exposure = sponsorships.length > 0
|
||||
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
|
||||
: 0;
|
||||
|
||||
this.presenter.present({
|
||||
const dto: SponsorDashboardDTO = {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics: {
|
||||
@@ -162,6 +172,8 @@ export class GetSponsorDashboardUseCase {
|
||||
totalInvestment,
|
||||
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,11 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { ISponsorSponsorshipsPresenter } from '../presenters/ISponsorSponsorshipsPresenter';
|
||||
import type {
|
||||
ISponsorSponsorshipsPresenter,
|
||||
SponsorSponsorshipsViewModel,
|
||||
} from '../presenters/ISponsorSponsorshipsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetSponsorSponsorshipsQueryParams {
|
||||
sponsorId: string;
|
||||
@@ -62,7 +66,9 @@ export interface SponsorSponsorshipsDTO {
|
||||
};
|
||||
}
|
||||
|
||||
export class GetSponsorSponsorshipsUseCase {
|
||||
export class GetSponsorSponsorshipsUseCase
|
||||
implements UseCase<GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO | null, SponsorSponsorshipsViewModel, ISponsorSponsorshipsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
|
||||
@@ -70,15 +76,19 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly presenter: ISponsorSponsorshipsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<void> {
|
||||
async execute(
|
||||
params: GetSponsorSponsorshipsQueryParams,
|
||||
presenter: ISponsorSponsorshipsPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
||||
if (!sponsor) {
|
||||
this.presenter.present(null);
|
||||
presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,7 +160,7 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
|
||||
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
|
||||
|
||||
this.presenter.present({
|
||||
const dto: SponsorSponsorshipsDTO = {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
@@ -161,6 +171,8 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
totalPlatformFees,
|
||||
currency: 'USD',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,31 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { ITeamDetailsPresenter } from '../presenters/ITeamDetailsPresenter';
|
||||
import type {
|
||||
ITeamDetailsPresenter,
|
||||
TeamDetailsResultDTO,
|
||||
TeamDetailsViewModel,
|
||||
} from '../presenters/ITeamDetailsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team details.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetTeamDetailsUseCase {
|
||||
export class GetTeamDetailsUseCase
|
||||
implements UseCase<{ teamId: string; driverId: string }, TeamDetailsResultDTO, TeamDetailsViewModel, ITeamDetailsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
public readonly presenter: ITeamDetailsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(teamId: string, driverId: string): Promise<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);
|
||||
if (!team) {
|
||||
throw new Error('Team not found');
|
||||
@@ -21,6 +33,12 @@ export class GetTeamDetailsUseCase {
|
||||
|
||||
const membership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
|
||||
this.presenter.present(team, membership, driverId);
|
||||
const dto: TeamDetailsResultDTO = {
|
||||
team,
|
||||
membership,
|
||||
driverId,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ export class PreviewLeagueScheduleUseCase {
|
||||
execute(params: PreviewLeagueScheduleQueryParams): void {
|
||||
const seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule);
|
||||
|
||||
if (!seasonSchedule) {
|
||||
throw new Error('Invalid schedule data');
|
||||
}
|
||||
|
||||
const maxRounds =
|
||||
params.maxRounds && params.maxRounds > 0
|
||||
? Math.min(params.maxRounds, seasonSchedule.plannedRounds)
|
||||
@@ -46,8 +50,11 @@ export class PreviewLeagueScheduleUseCase {
|
||||
return 'No rounds scheduled.';
|
||||
}
|
||||
|
||||
const first = new Date(rounds[0].scheduledAt);
|
||||
const last = new Date(rounds[rounds.length - 1].scheduledAt);
|
||||
const firstRound = rounds[0]!;
|
||||
const lastRound = rounds[rounds.length - 1]!;
|
||||
|
||||
const first = new Date(firstRound.scheduledAt);
|
||||
const last = new Date(lastRound.scheduledAt);
|
||||
|
||||
const firstDate = first.toISOString().slice(0, 10);
|
||||
const lastDate = last.toISOString().slice(0, 10);
|
||||
|
||||
@@ -22,10 +22,12 @@ export class UpdateTeamUseCase {
|
||||
throw new Error('Team not found');
|
||||
}
|
||||
|
||||
const updated: Team = {
|
||||
...existing,
|
||||
...updates,
|
||||
};
|
||||
const updated = existing.update({
|
||||
...(updates.name !== undefined && { name: updates.name }),
|
||||
...(updates.tag !== undefined && { tag: updates.tag }),
|
||||
...(updates.description !== undefined && { description: updates.description }),
|
||||
...(updates.leagues !== undefined && { leagues: updates.leagues }),
|
||||
});
|
||||
|
||||
await this.teamRepository.update(updated);
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ export class Car implements IEntity<string> {
|
||||
readonly carClass: CarClass;
|
||||
readonly license: CarLicense;
|
||||
readonly year: number;
|
||||
readonly horsepower?: number;
|
||||
readonly weight?: number;
|
||||
readonly imageUrl?: string;
|
||||
readonly horsepower: number | undefined;
|
||||
readonly weight: number | undefined;
|
||||
readonly imageUrl: string | undefined;
|
||||
readonly gameId: string;
|
||||
|
||||
private constructor(props: {
|
||||
|
||||
@@ -13,7 +13,7 @@ export class Driver implements IEntity<string> {
|
||||
readonly iracingId: string;
|
||||
readonly name: string;
|
||||
readonly country: string;
|
||||
readonly bio?: string;
|
||||
readonly bio: string | undefined;
|
||||
readonly joinedAt: Date;
|
||||
|
||||
private constructor(props: {
|
||||
@@ -92,14 +92,18 @@ export class Driver implements IEntity<string> {
|
||||
update(props: Partial<{
|
||||
name: string;
|
||||
country: string;
|
||||
bio: string;
|
||||
bio?: string;
|
||||
}>): Driver {
|
||||
const nextName = props.name ?? this.name;
|
||||
const nextCountry = props.country ?? this.country;
|
||||
const nextBio = props.bio ?? this.bio;
|
||||
|
||||
return new Driver({
|
||||
id: this.id,
|
||||
iracingId: this.iracingId,
|
||||
name: props.name ?? this.name,
|
||||
country: props.country ?? this.country,
|
||||
bio: props.bio ?? this.bio,
|
||||
name: nextName,
|
||||
country: nextCountry,
|
||||
...(nextBio !== undefined ? { bio: nextBio } : {}),
|
||||
joinedAt: this.joinedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export class League implements IEntity<string> {
|
||||
readonly ownerId: string;
|
||||
readonly settings: LeagueSettings;
|
||||
readonly createdAt: Date;
|
||||
readonly socialLinks?: LeagueSocialLinks;
|
||||
readonly socialLinks: LeagueSocialLinks | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
@@ -140,6 +140,8 @@ export class League implements IEntity<string> {
|
||||
stewarding: defaultStewardingSettings,
|
||||
};
|
||||
|
||||
const socialLinks = props.socialLinks;
|
||||
|
||||
return new League({
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
@@ -147,7 +149,7 @@ export class League implements IEntity<string> {
|
||||
ownerId: props.ownerId,
|
||||
settings: { ...defaultSettings, ...props.settings },
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
socialLinks: props.socialLinks,
|
||||
...(socialLinks !== undefined ? { socialLinks } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,7 +191,7 @@ export class League implements IEntity<string> {
|
||||
description: string;
|
||||
ownerId: string;
|
||||
settings: LeagueSettings;
|
||||
socialLinks: LeagueSocialLinks | undefined;
|
||||
socialLinks?: LeagueSocialLinks;
|
||||
}>): League {
|
||||
return new League({
|
||||
id: this.id,
|
||||
@@ -198,7 +200,11 @@ export class League implements IEntity<string> {
|
||||
ownerId: props.ownerId ?? this.ownerId,
|
||||
settings: props.settings ?? this.settings,
|
||||
createdAt: this.createdAt,
|
||||
socialLinks: props.socialLinks ?? this.socialLinks,
|
||||
...(props.socialLinks !== undefined
|
||||
? { socialLinks: props.socialLinks }
|
||||
: this.socialLinks !== undefined
|
||||
? { socialLinks: this.socialLinks }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -102,12 +102,16 @@ export class Penalty implements IEntity<string> {
|
||||
if (this.props.status === 'overturned') {
|
||||
throw new RacingDomainInvariantError('Cannot apply an overturned penalty');
|
||||
}
|
||||
return new Penalty({
|
||||
const base: PenaltyProps = {
|
||||
...this.props,
|
||||
status: 'applied',
|
||||
appliedAt: new Date(),
|
||||
notes,
|
||||
});
|
||||
};
|
||||
|
||||
const next: PenaltyProps =
|
||||
notes !== undefined ? { ...base, notes } : base;
|
||||
|
||||
return Penalty.create(next);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -153,14 +153,18 @@ export class Protest implements IEntity<string> {
|
||||
if (!statement?.trim()) {
|
||||
throw new RacingDomainValidationError('Defense statement is required');
|
||||
}
|
||||
const defenseBase: ProtestDefense = {
|
||||
statement: statement.trim(),
|
||||
submittedAt: new Date(),
|
||||
};
|
||||
|
||||
const nextDefense: ProtestDefense =
|
||||
videoUrl !== undefined ? { ...defenseBase, videoUrl } : defenseBase;
|
||||
|
||||
return new Protest({
|
||||
...this.props,
|
||||
status: 'under_review',
|
||||
defense: {
|
||||
statement: statement.trim(),
|
||||
videoUrl,
|
||||
submittedAt: new Date(),
|
||||
},
|
||||
defense: nextDefense,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ export class Race implements IEntity<string> {
|
||||
readonly leagueId: string;
|
||||
readonly scheduledAt: Date;
|
||||
readonly track: string;
|
||||
readonly trackId?: string;
|
||||
readonly trackId: string | undefined;
|
||||
readonly car: string;
|
||||
readonly carId?: string;
|
||||
readonly carId: string | undefined;
|
||||
readonly sessionType: SessionType;
|
||||
readonly status: RaceStatus;
|
||||
readonly strengthOfField?: number;
|
||||
readonly registeredCount?: number;
|
||||
readonly maxParticipants?: number;
|
||||
readonly strengthOfField: number | undefined;
|
||||
readonly registeredCount: number | undefined;
|
||||
readonly maxParticipants: number | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
@@ -127,10 +127,34 @@ export class Race implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Only scheduled races can be started');
|
||||
}
|
||||
|
||||
return new Race({
|
||||
...this,
|
||||
status: 'running',
|
||||
});
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: 'running' as RaceStatus,
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const withSof =
|
||||
this.strengthOfField !== undefined
|
||||
? { ...withCarId, strengthOfField: this.strengthOfField }
|
||||
: withCarId;
|
||||
const withRegistered =
|
||||
this.registeredCount !== undefined
|
||||
? { ...withSof, registeredCount: this.registeredCount }
|
||||
: withSof;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withRegistered, maxParticipants: this.maxParticipants }
|
||||
: withRegistered;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,10 +169,34 @@ export class Race implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Cannot complete a cancelled race');
|
||||
}
|
||||
|
||||
return new Race({
|
||||
...this,
|
||||
status: 'completed',
|
||||
});
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: 'completed' as RaceStatus,
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const withSof =
|
||||
this.strengthOfField !== undefined
|
||||
? { ...withCarId, strengthOfField: this.strengthOfField }
|
||||
: withCarId;
|
||||
const withRegistered =
|
||||
this.registeredCount !== undefined
|
||||
? { ...withSof, registeredCount: this.registeredCount }
|
||||
: withSof;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withRegistered, maxParticipants: this.maxParticipants }
|
||||
: withRegistered;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,21 +211,62 @@ export class Race implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Race is already cancelled');
|
||||
}
|
||||
|
||||
return new Race({
|
||||
...this,
|
||||
status: 'cancelled',
|
||||
});
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: 'cancelled' as RaceStatus,
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const withSof =
|
||||
this.strengthOfField !== undefined
|
||||
? { ...withCarId, strengthOfField: this.strengthOfField }
|
||||
: withCarId;
|
||||
const withRegistered =
|
||||
this.registeredCount !== undefined
|
||||
? { ...withSof, registeredCount: this.registeredCount }
|
||||
: withSof;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withRegistered, maxParticipants: this.maxParticipants }
|
||||
: withRegistered;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SOF and participant count
|
||||
*/
|
||||
updateField(strengthOfField: number, registeredCount: number): Race {
|
||||
return new Race({
|
||||
...this,
|
||||
const base = {
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
scheduledAt: this.scheduledAt,
|
||||
track: this.track,
|
||||
car: this.car,
|
||||
sessionType: this.sessionType,
|
||||
status: this.status,
|
||||
strengthOfField,
|
||||
registeredCount,
|
||||
});
|
||||
};
|
||||
|
||||
const withTrackId =
|
||||
this.trackId !== undefined ? { ...base, trackId: this.trackId } : base;
|
||||
const withCarId =
|
||||
this.carId !== undefined ? { ...withTrackId, carId: this.carId } : withTrackId;
|
||||
const props =
|
||||
this.maxParticipants !== undefined
|
||||
? { ...withCarId, maxParticipants: this.maxParticipants }
|
||||
: withCarId;
|
||||
|
||||
return Race.create(props);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,11 +8,11 @@ export class Season implements IEntity<string> {
|
||||
readonly leagueId: string;
|
||||
readonly gameId: string;
|
||||
readonly name: string;
|
||||
readonly year?: number;
|
||||
readonly order?: number;
|
||||
readonly year: number | undefined;
|
||||
readonly order: number | undefined;
|
||||
readonly status: SeasonStatus;
|
||||
readonly startDate?: Date;
|
||||
readonly endDate?: Date;
|
||||
readonly startDate: Date | undefined;
|
||||
readonly endDate: Date | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
|
||||
@@ -33,8 +33,8 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
readonly pricing: Money;
|
||||
readonly status: SponsorshipStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly activatedAt?: Date;
|
||||
readonly description?: string;
|
||||
readonly activatedAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
|
||||
private constructor(props: SeasonSponsorshipProps) {
|
||||
this.id = props.id;
|
||||
@@ -105,11 +105,23 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Cannot activate a cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
return new SeasonSponsorship({
|
||||
...this,
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'active',
|
||||
createdAt: this.createdAt,
|
||||
activatedAt: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,10 +132,27 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('SeasonSponsorship is already cancelled');
|
||||
}
|
||||
|
||||
return new SeasonSponsorship({
|
||||
...this,
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'cancelled',
|
||||
});
|
||||
createdAt: this.createdAt,
|
||||
};
|
||||
|
||||
const withActivated =
|
||||
this.activatedAt !== undefined
|
||||
? { ...base, activatedAt: this.activatedAt }
|
||||
: base;
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...withActivated, description: this.description }
|
||||
: withActivated;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,8 +20,8 @@ export class Sponsor implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly contactEmail: string;
|
||||
readonly logoUrl?: string;
|
||||
readonly websiteUrl?: string;
|
||||
readonly logoUrl: string | undefined;
|
||||
readonly websiteUrl: string | undefined;
|
||||
readonly createdAt: Date;
|
||||
|
||||
private constructor(props: SponsorProps) {
|
||||
@@ -35,11 +35,23 @@ export class Sponsor implements IEntity<string> {
|
||||
|
||||
static create(props: Omit<SponsorProps, 'createdAt'> & { createdAt?: Date }): Sponsor {
|
||||
this.validate(props);
|
||||
|
||||
return new Sponsor({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
});
|
||||
|
||||
const { createdAt, ...rest } = props;
|
||||
const base = {
|
||||
id: rest.id,
|
||||
name: rest.name,
|
||||
contactEmail: rest.contactEmail,
|
||||
createdAt: createdAt ?? new Date(),
|
||||
};
|
||||
|
||||
const withLogo =
|
||||
rest.logoUrl !== undefined ? { ...base, logoUrl: rest.logoUrl } : base;
|
||||
const withWebsite =
|
||||
rest.websiteUrl !== undefined
|
||||
? { ...withLogo, websiteUrl: rest.websiteUrl }
|
||||
: withLogo;
|
||||
|
||||
return new Sponsor(withWebsite);
|
||||
}
|
||||
|
||||
private static validate(props: Omit<SponsorProps, 'createdAt'>): void {
|
||||
@@ -80,18 +92,30 @@ export class Sponsor implements IEntity<string> {
|
||||
update(props: Partial<{
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
logoUrl: string | undefined;
|
||||
websiteUrl: string | undefined;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}>): Sponsor {
|
||||
const updated = {
|
||||
const updatedBase = {
|
||||
id: this.id,
|
||||
name: props.name ?? this.name,
|
||||
contactEmail: props.contactEmail ?? this.contactEmail,
|
||||
logoUrl: props.logoUrl !== undefined ? props.logoUrl : this.logoUrl,
|
||||
websiteUrl: props.websiteUrl !== undefined ? props.websiteUrl : this.websiteUrl,
|
||||
createdAt: this.createdAt,
|
||||
};
|
||||
|
||||
const withLogo =
|
||||
props.logoUrl !== undefined
|
||||
? { ...updatedBase, logoUrl: props.logoUrl }
|
||||
: this.logoUrl !== undefined
|
||||
? { ...updatedBase, logoUrl: this.logoUrl }
|
||||
: updatedBase;
|
||||
|
||||
const updated =
|
||||
props.websiteUrl !== undefined
|
||||
? { ...withLogo, websiteUrl: props.websiteUrl }
|
||||
: this.websiteUrl !== undefined
|
||||
? { ...withLogo, websiteUrl: this.websiteUrl }
|
||||
: withLogo;
|
||||
|
||||
Sponsor.validate(updated);
|
||||
return new Sponsor(updated);
|
||||
}
|
||||
|
||||
@@ -36,12 +36,12 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
readonly entityId: string;
|
||||
readonly tier: SponsorshipTier;
|
||||
readonly offeredAmount: Money;
|
||||
readonly message?: string;
|
||||
readonly message: string | undefined;
|
||||
readonly status: SponsorshipRequestStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly respondedAt?: Date;
|
||||
readonly respondedBy?: string;
|
||||
readonly rejectionReason?: string;
|
||||
readonly respondedAt: Date | undefined;
|
||||
readonly respondedBy: string | undefined;
|
||||
readonly rejectionReason: string | undefined;
|
||||
|
||||
private constructor(props: SponsorshipRequestProps) {
|
||||
this.id = props.id;
|
||||
@@ -113,12 +113,28 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
throw new RacingDomainValidationError('respondedBy is required when accepting');
|
||||
}
|
||||
|
||||
return new SponsorshipRequest({
|
||||
...this,
|
||||
const base: SponsorshipRequestProps = {
|
||||
id: this.id,
|
||||
sponsorId: this.sponsorId,
|
||||
entityType: this.entityType,
|
||||
entityId: this.entityId,
|
||||
tier: this.tier,
|
||||
offeredAmount: this.offeredAmount,
|
||||
status: 'accepted',
|
||||
createdAt: this.createdAt,
|
||||
respondedAt: new Date(),
|
||||
respondedBy,
|
||||
});
|
||||
};
|
||||
|
||||
const withMessage =
|
||||
this.message !== undefined ? { ...base, message: this.message } : base;
|
||||
|
||||
const next: SponsorshipRequestProps =
|
||||
this.rejectionReason !== undefined
|
||||
? { ...withMessage, rejectionReason: this.rejectionReason }
|
||||
: withMessage;
|
||||
|
||||
return new SponsorshipRequest(next);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,13 +149,26 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
throw new RacingDomainValidationError('respondedBy is required when rejecting');
|
||||
}
|
||||
|
||||
return new SponsorshipRequest({
|
||||
...this,
|
||||
const base: SponsorshipRequestProps = {
|
||||
id: this.id,
|
||||
sponsorId: this.sponsorId,
|
||||
entityType: this.entityType,
|
||||
entityId: this.entityId,
|
||||
tier: this.tier,
|
||||
offeredAmount: this.offeredAmount,
|
||||
status: 'rejected',
|
||||
createdAt: this.createdAt,
|
||||
respondedAt: new Date(),
|
||||
respondedBy,
|
||||
rejectionReason: reason,
|
||||
});
|
||||
};
|
||||
|
||||
const withMessage =
|
||||
this.message !== undefined ? { ...base, message: this.message } : base;
|
||||
|
||||
const next: SponsorshipRequestProps =
|
||||
reason !== undefined ? { ...withMessage, rejectionReason: reason } : withMessage;
|
||||
|
||||
return new SponsorshipRequest(next);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,11 +179,34 @@ export class SponsorshipRequest implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError(`Cannot withdraw a ${this.status} sponsorship request`);
|
||||
}
|
||||
|
||||
return new SponsorshipRequest({
|
||||
...this,
|
||||
const base: SponsorshipRequestProps = {
|
||||
id: this.id,
|
||||
sponsorId: this.sponsorId,
|
||||
entityType: this.entityType,
|
||||
entityId: this.entityId,
|
||||
tier: this.tier,
|
||||
offeredAmount: this.offeredAmount,
|
||||
status: 'withdrawn',
|
||||
createdAt: this.createdAt,
|
||||
respondedAt: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
const withRespondedBy =
|
||||
this.respondedBy !== undefined
|
||||
? { ...base, respondedBy: this.respondedBy }
|
||||
: base;
|
||||
|
||||
const withMessage =
|
||||
this.message !== undefined
|
||||
? { ...withRespondedBy, message: this.message }
|
||||
: withRespondedBy;
|
||||
|
||||
const next: SponsorshipRequestProps =
|
||||
this.rejectionReason !== undefined
|
||||
? { ...withMessage, rejectionReason: this.rejectionReason }
|
||||
: withMessage;
|
||||
|
||||
return new SponsorshipRequest(next);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainError } from '../errors/RacingDomainError';
|
||||
import { RacingDomainError, RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export class Standing implements IEntity<string> {
|
||||
@@ -104,12 +104,17 @@ export class Standing implements IEntity<string> {
|
||||
*/
|
||||
updatePosition(position: number): Standing {
|
||||
if (!Number.isInteger(position) || position < 1) {
|
||||
throw new RacingDomainError('Position must be a positive integer');
|
||||
throw new RacingDomainValidationError('Position must be a positive integer');
|
||||
}
|
||||
|
||||
return new Standing({
|
||||
...this,
|
||||
return Standing.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
driverId: this.driverId,
|
||||
points: this.points,
|
||||
wins: this.wins,
|
||||
position,
|
||||
racesCompleted: this.racesCompleted,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Track implements IEntity<string> {
|
||||
readonly difficulty: TrackDifficulty;
|
||||
readonly lengthKm: number;
|
||||
readonly turns: number;
|
||||
readonly imageUrl?: string;
|
||||
readonly imageUrl: string | undefined;
|
||||
readonly gameId: string;
|
||||
|
||||
private constructor(props: {
|
||||
@@ -32,7 +32,7 @@ export class Track implements IEntity<string> {
|
||||
difficulty: TrackDifficulty;
|
||||
lengthKm: number;
|
||||
turns: number;
|
||||
imageUrl?: string;
|
||||
imageUrl?: string | undefined;
|
||||
gameId: string;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
@@ -64,7 +64,7 @@ export class Track implements IEntity<string> {
|
||||
}): Track {
|
||||
this.validate(props);
|
||||
|
||||
return new Track({
|
||||
const base = {
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
shortName: props.shortName ?? props.name.slice(0, 3).toUpperCase(),
|
||||
@@ -73,9 +73,13 @@ export class Track implements IEntity<string> {
|
||||
difficulty: props.difficulty ?? 'intermediate',
|
||||
lengthKm: props.lengthKm,
|
||||
turns: props.turns,
|
||||
imageUrl: props.imageUrl,
|
||||
gameId: props.gameId,
|
||||
});
|
||||
};
|
||||
|
||||
const withImage =
|
||||
props.imageUrl !== undefined ? { ...base, imageUrl: props.imageUrl } : base;
|
||||
|
||||
return new Track(withImage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,11 @@ export interface ITeamMembershipRepository {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user