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

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