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

View File

@@ -255,7 +255,18 @@ export function configureDIContainer(): void {
}
// Overlay Sync Service - create singleton instance directly
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 {

View File

@@ -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) {

View File

@@ -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),

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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="••••••••"

View File

@@ -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="••••••••"

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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();

View File

@@ -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

View File

@@ -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');
}

View File

@@ -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 ?? ''}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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
View File

@@ -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';

View File

@@ -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
)
);

View File

@@ -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);
},
};
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -1,21 +1,20 @@
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { SkillLevel } from '@gridpilot/racing/domain/services/SkillLevelService';
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
import 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;
}
}

View File

@@ -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 {

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -1,14 +1,25 @@
import type { ISponsorDashboardPresenter } from '@gridpilot/racing/application/presenters/ISponsorDashboardPresenter';
import type {
ISponsorDashboardPresenter,
SponsorDashboardViewModel,
} from '@gridpilot/racing/application/presenters/ISponsorDashboardPresenter';
import type { SponsorDashboardDTO } from '@gridpilot/racing/application/use-cases/GetSponsorDashboardUseCase';
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;
}
}

View File

@@ -1,14 +1,25 @@
import type { ISponsorSponsorshipsPresenter } from '@gridpilot/racing/application/presenters/ISponsorSponsorshipsPresenter';
import type {
ISponsorSponsorshipsPresenter,
SponsorSponsorshipsViewModel,
} from '@gridpilot/racing/application/presenters/ISponsorSponsorshipsPresenter';
import type { SponsorSponsorshipsDTO } from '@gridpilot/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
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;
}
}

View File

@@ -1,18 +1,18 @@
import type { Team } from '@gridpilot/racing/domain/entities/Team';
import type { TeamMembership } from '@gridpilot/racing/domain/types/TeamMembership';
import type {
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;
}
}

View File

@@ -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"]
}

View File

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