This commit is contained in:
2025-12-10 18:28:32 +01:00
parent 6d61be9c51
commit 1303a14493
108 changed files with 3366 additions and 1559 deletions

View File

@@ -4,7 +4,8 @@ import {
type LeagueScheduleDTO,
type LeagueSchedulePreviewDTO,
} from '@gridpilot/racing/application';
import { getPreviewLeagueScheduleQuery } from '@/lib/di-container';
import { getPreviewLeagueScheduleUseCase } from '@/lib/di-container';
import { LeagueSchedulePreviewPresenter } from '@/lib/presenters/LeagueSchedulePreviewPresenter';
interface RequestBody {
seasonStartDate?: string;
@@ -73,11 +74,16 @@ export async function POST(request: NextRequest) {
const schedule = toLeagueScheduleDTO(json);
const query = getPreviewLeagueScheduleQuery();
const preview: LeagueSchedulePreviewDTO = await query.execute({
const presenter = new LeagueSchedulePreviewPresenter();
const useCase = getPreviewLeagueScheduleUseCase();
useCase.execute({
schedule,
maxRounds: 10,
});
const preview = presenter.getData();
if (!preview) {
return NextResponse.json({ error: 'Failed to generate preview' }, { status: 500 });
}
return NextResponse.json(preview, { status: 200 });
} catch (error) {

View File

@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { getGetSponsorDashboardQuery } from '@/lib/di-container';
import { getGetSponsorDashboardUseCase } from '@/lib/di-container';
import { SponsorDashboardPresenter } from '@/lib/presenters/SponsorDashboardPresenter';
export async function GET(request: NextRequest) {
try {
@@ -16,8 +17,10 @@ export async function GET(request: NextRequest) {
);
}
const query = getGetSponsorDashboardQuery();
const dashboard = await query.execute({ sponsorId });
const presenter = new SponsorDashboardPresenter();
const useCase = getGetSponsorDashboardUseCase();
await useCase.execute({ sponsorId });
const dashboard = presenter.getData();
if (!dashboard) {
return NextResponse.json(

View File

@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { getGetSponsorSponsorshipsQuery } from '@/lib/di-container';
import { getGetSponsorSponsorshipsUseCase } from '@/lib/di-container';
import { SponsorSponsorshipsPresenter } from '@/lib/presenters/SponsorSponsorshipsPresenter';
export async function GET(request: NextRequest) {
try {
@@ -16,8 +17,10 @@ export async function GET(request: NextRequest) {
);
}
const query = getGetSponsorSponsorshipsQuery();
const sponsorships = await query.execute({ sponsorId });
const presenter = new SponsorSponsorshipsPresenter();
const useCase = getGetSponsorSponsorshipsUseCase();
await useCase.execute({ sponsorId });
const sponsorships = presenter.getData();
if (!sponsorships) {
return NextResponse.json(

View File

@@ -40,11 +40,11 @@ import {
getDriverRepository,
getDriverStats,
getAllDriverRankings,
getGetDriverTeamQuery,
getGetDriverTeamUseCase,
getSocialRepository,
getImageService,
getGetAllTeamsQuery,
getGetTeamMembersQuery,
getGetAllTeamsUseCase,
getGetTeamMembersUseCase,
} from '@/lib/di-container';
import { Driver, EntityMappers, type Team } from '@gridpilot/racing';
import type { DriverDTO } from '@gridpilot/racing';
@@ -382,18 +382,23 @@ export default function DriverDetailPage({
setDriver(driverDto);
// Load team data
const teamQuery = getGetDriverTeamQuery();
const teamResult = await teamQuery.execute({ driverId });
setTeamData(teamResult);
const teamUseCase = getGetDriverTeamUseCase();
await teamUseCase.execute({ driverId });
const teamViewModel = teamUseCase.presenter.getViewModel();
setTeamData(teamViewModel.result);
// Load ALL team memberships
const allTeamsQuery = getGetAllTeamsQuery();
const allTeams = await allTeamsQuery.execute();
const membershipsQuery = getGetTeamMembersQuery();
const allTeamsUseCase = getGetAllTeamsUseCase();
await allTeamsUseCase.execute();
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
const allTeams = allTeamsViewModel.teams;
const membershipsUseCase = getGetTeamMembersUseCase();
const memberships: TeamMembershipInfo[] = [];
for (const team of allTeams) {
const members = await membershipsQuery.execute({ teamId: team.id });
await membershipsUseCase.execute({ teamId: team.id });
const membersViewModel = membershipsUseCase.presenter.getViewModel();
const members = membersViewModel.members;
const membership = members.find((m) => m.driverId === driverId);
if (membership) {
memberships.push({

View File

@@ -27,27 +27,15 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading';
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService } from '@/lib/di-container';
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import Image from 'next/image';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
interface DriverListItem {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
isActive: boolean;
rank: number;
}
type DriverListItem = DriverLeaderboardItemViewModel;
// ============================================================================
// DEMO DATA
@@ -87,7 +75,6 @@ interface FeaturedDriverCardProps {
}
function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) {
const imageService = getImageService();
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
const getBorderColor = (pos: number) => {
@@ -131,7 +118,7 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro
{/* Avatar & Name */}
<div className="flex items-center gap-4 mb-4">
<div className="relative w-16 h-16 rounded-full overflow-hidden border-2 border-charcoal-outline group-hover:border-primary-blue transition-colors">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
<div>
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors">
@@ -236,7 +223,6 @@ interface LeaderboardPreviewProps {
function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps) {
const router = useRouter();
const imageService = getImageService();
const top5 = drivers.slice(0, 5);
const getMedalColor = (position: number) => {
@@ -300,7 +286,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps)
{/* Avatar */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
{/* Info */}
@@ -345,7 +331,6 @@ interface RecentActivityProps {
}
function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
const imageService = getImageService();
const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6);
return (
@@ -371,7 +356,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
className="p-3 rounded-xl bg-iron-gray/40 border border-charcoal-outline hover:border-performance-green/40 transition-all group text-center"
>
<div className="relative w-12 h-12 mx-auto rounded-full overflow-hidden border-2 border-charcoal-outline mb-2">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
<div className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-performance-green border-2 border-iron-gray" />
</div>
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
@@ -395,57 +380,20 @@ export default function DriversPage() {
const [drivers, setDrivers] = useState<DriverListItem[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [totalRaces, setTotalRaces] = useState(0);
const [totalWins, setTotalWins] = useState(0);
const [activeCount, setActiveCount] = useState(0);
useEffect(() => {
const load = async () => {
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const rankings = getAllDriverRankings();
const useCase = getGetDriversLeaderboardUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
const items: DriverListItem[] = allDrivers.map((driver) => {
const stats = getDriverStats(driver.id);
const rating = stats?.rating ?? 0;
const wins = stats?.wins ?? 0;
const podiums = stats?.podiums ?? 0;
const totalRaces = stats?.totalRaces ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
effectiveRank = stats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel: SkillLevel =
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
const isActive = rankings.some((r) => r.driverId === driver.id);
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
racesCompleted: totalRaces,
wins,
podiums,
isActive,
rank: effectiveRank,
};
});
// Sort by rank
items.sort((a, b) => {
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
return rankA - rankB || b.rating - a.rating;
});
setDrivers(items);
setDrivers(viewModel.drivers);
setTotalRaces(viewModel.totalRaces);
setTotalWins(viewModel.totalWins);
setActiveCount(viewModel.activeCount);
setLoading(false);
};
@@ -465,10 +413,6 @@ export default function DriversPage() {
);
});
// Stats
const totalRaces = drivers.reduce((sum, d) => sum + d.racesCompleted, 0);
const totalWins = drivers.reduce((sum, d) => sum + d.wins, 0);
const activeCount = drivers.filter((d) => d.isActive).length;
// Featured drivers (top 4)
const featuredDrivers = filteredDrivers.slice(0, 4);

View File

@@ -19,27 +19,17 @@ import {
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService } from '@/lib/di-container';
import { getGetDriversLeaderboardUseCase } from '@/lib/di-container';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import Image from 'next/image';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
interface DriverListItem {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
rank: number;
}
type DriverListItem = DriverLeaderboardItemViewModel;
// ============================================================================
// SKILL LEVEL CONFIG
@@ -81,7 +71,6 @@ interface TopThreePodiumProps {
}
function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
const imageService = getImageService();
const top3 = drivers.slice(0, 3);
if (top3.length < 3) return null;
@@ -122,7 +111,7 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
{/* Avatar */}
<div className={`relative ${position === 1 ? 'w-24 h-24 lg:w-28 lg:h-28' : 'w-20 h-20 lg:w-24 lg:h-24'} rounded-full overflow-hidden border-4 ${position === 1 ? 'border-yellow-400 shadow-[0_0_30px_rgba(250,204,21,0.3)]' : position === 2 ? 'border-gray-300' : 'border-amber-600'} group-hover:scale-105 transition-transform`}>
<Image
src={imageService.getDriverAvatar(driver.id)}
src={driver.avatarUrl}
alt={driver.name}
fill
className="object-cover"
@@ -178,7 +167,6 @@ function TopThreePodium({ drivers, onDriverClick }: TopThreePodiumProps) {
export default function DriverLeaderboardPage() {
const router = useRouter();
const imageService = getImageService();
const [drivers, setDrivers] = useState<DriverListItem[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
@@ -188,44 +176,10 @@ export default function DriverLeaderboardPage() {
useEffect(() => {
const load = async () => {
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const rankings = getAllDriverRankings();
const items: DriverListItem[] = allDrivers.map((driver) => {
const stats = getDriverStats(driver.id);
const rating = stats?.rating ?? 0;
const wins = stats?.wins ?? 0;
const podiums = stats?.podiums ?? 0;
const totalRaces = stats?.totalRaces ?? 0;
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
effectiveRank = stats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel: SkillLevel =
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
racesCompleted: totalRaces,
wins,
podiums,
rank: effectiveRank,
};
});
setDrivers(items);
const useCase = getGetDriversLeaderboardUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
setDrivers(viewModel.drivers);
setLoading(false);
};
@@ -443,7 +397,7 @@ export default function DriverLeaderboardPage() {
{/* Driver Info */}
<div className="col-span-5 lg:col-span-4 flex items-center gap-3">
<div className="relative w-10 h-10 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
<div className="min-w-0">
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">

View File

@@ -20,36 +20,18 @@ import {
} from 'lucide-react';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import { getDriverRepository, getDriverStats, getAllDriverRankings, getImageService, getGetAllTeamsQuery, getGetTeamMembersQuery } from '@/lib/di-container';
import { getGetDriversLeaderboardUseCase, getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
import type { TeamLeaderboardItemViewModel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
import Image from 'next/image';
import type { Team } from '@gridpilot/racing';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
type DriverListItem = DriverLeaderboardItemViewModel;
interface DriverListItem {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
wins: number;
podiums: number;
rank: number;
}
interface TeamDisplayData {
id: string;
name: string;
memberCount: number;
rating: number | null;
totalWins: number;
totalRaces: number;
performanceLevel: SkillLevel;
}
type TeamDisplayData = TeamLeaderboardItemViewModel;
// ============================================================================
// SKILL LEVEL CONFIG
@@ -80,7 +62,6 @@ interface DriverLeaderboardPreviewProps {
function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardPreviewProps) {
const router = useRouter();
const imageService = getImageService();
const top10 = drivers.slice(0, 10);
const getMedalColor = (position: number) => {
@@ -144,7 +125,7 @@ function DriverLeaderboardPreview({ drivers, onDriverClick }: DriverLeaderboardP
{/* Avatar */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={imageService.getDriverAvatar(driver.id)} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
</div>
{/* Info */}
@@ -189,7 +170,6 @@ interface TeamLeaderboardPreviewProps {
function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeaderboardPreviewProps) {
const router = useRouter();
const imageService = getImageService();
const top5 = [...teams]
.filter((t) => t.rating !== null)
.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0))
@@ -304,99 +284,16 @@ export default function LeaderboardsPage() {
useEffect(() => {
const load = async () => {
try {
// Load drivers
const driverRepo = getDriverRepository();
const allDrivers = await driverRepo.findAll();
const rankings = getAllDriverRankings();
const driversUseCase = getGetDriversLeaderboardUseCase();
const teamsUseCase = getGetTeamsLeaderboardUseCase();
await driversUseCase.execute();
await teamsUseCase.execute();
const driverItems: DriverListItem[] = allDrivers.map((driver) => {
const stats = getDriverStats(driver.id);
const rating = stats?.rating ?? 0;
const wins = stats?.wins ?? 0;
const podiums = stats?.podiums ?? 0;
const driversViewModel = driversUseCase.presenter.getViewModel();
const teamsViewModel = teamsUseCase.presenter.getViewModel();
let effectiveRank = Number.POSITIVE_INFINITY;
if (typeof stats?.overallRank === 'number' && stats.overallRank > 0) {
effectiveRank = stats.overallRank;
} else {
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
if (indexInGlobal !== -1) {
effectiveRank = indexInGlobal + 1;
}
}
const skillLevel: SkillLevel =
rating >= 3000 ? 'pro' : rating >= 2500 ? 'advanced' : rating >= 1800 ? 'intermediate' : 'beginner';
return {
id: driver.id,
name: driver.name,
rating,
skillLevel,
nationality: driver.country,
wins,
podiums,
rank: effectiveRank,
};
});
// Sort by rank
driverItems.sort((a, b) => {
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
return rankA - rankB || b.rating - a.rating;
});
// Load teams
const allTeamsQuery = getGetAllTeamsQuery();
const teamMembersQuery = getGetTeamMembersQuery();
const allTeams = await allTeamsQuery.execute();
const teamData: TeamDisplayData[] = [];
await Promise.all(
allTeams.map(async (team: Team) => {
const memberships = await teamMembersQuery.execute({ teamId: team.id });
const memberCount = memberships.length;
let ratingSum = 0;
let ratingCount = 0;
let totalWins = 0;
let totalRaces = 0;
for (const membership of memberships) {
const stats = getDriverStats(membership.driverId);
if (!stats) continue;
if (typeof stats.rating === 'number') {
ratingSum += stats.rating;
ratingCount += 1;
}
totalWins += stats.wins ?? 0;
totalRaces += stats.totalRaces ?? 0;
}
const averageRating = ratingCount > 0 ? ratingSum / ratingCount : null;
let performanceLevel: SkillLevel = 'beginner';
if (averageRating !== null) {
if (averageRating >= 4500) performanceLevel = 'pro';
else if (averageRating >= 3000) performanceLevel = 'advanced';
else if (averageRating >= 2000) performanceLevel = 'intermediate';
}
teamData.push({
id: team.id,
name: team.name,
memberCount,
rating: averageRating,
totalWins,
totalRaces,
performanceLevel,
});
}),
);
setDrivers(driverItems);
setTeams(teamData);
setDrivers(driversViewModel.drivers);
setTeams(teamsViewModel.teams);
} catch (error) {
console.error('Failed to load leaderboard data:', error);
setDrivers([]);

View File

@@ -25,10 +25,10 @@ import {
getLeagueRepository,
getRaceRepository,
getDriverRepository,
getGetLeagueScoringConfigQuery,
getGetLeagueScoringConfigUseCase,
getDriverStats,
getAllDriverRankings,
getGetLeagueStatsQuery,
getGetLeagueStatsUseCase,
getSeasonRepository,
getSponsorRepository,
getSeasonSponsorshipRepository,
@@ -104,7 +104,7 @@ export default function LeagueDetailPage() {
const leagueRepo = getLeagueRepository();
const raceRepo = getRaceRepository();
const driverRepo = getDriverRepository();
const leagueStatsQuery = getGetLeagueStatsQuery();
const leagueStatsUseCase = getGetLeagueStatsUseCase();
const seasonRepo = getSeasonRepository();
const sponsorRepo = getSponsorRepository();
const sponsorshipRepo = getSeasonSponsorshipRepository();
@@ -124,9 +124,10 @@ export default function LeagueDetailPage() {
setOwner(ownerData);
// Load scoring configuration for the active season
const getLeagueScoringConfigQuery = getGetLeagueScoringConfigQuery();
const scoring = await getLeagueScoringConfigQuery.execute({ leagueId });
setScoringConfig(scoring);
const getLeagueScoringConfigUseCase = getGetLeagueScoringConfigUseCase();
await getLeagueScoringConfigUseCase.execute({ leagueId });
const scoringViewModel = getLeagueScoringConfigUseCase.presenter.getViewModel();
setScoringConfig(scoringViewModel);
// Load all drivers for standings and map to DTOs for UI components
const allDrivers = await driverRepo.findAll();
@@ -136,11 +137,12 @@ export default function LeagueDetailPage() {
setDrivers(driverDtos);
// Load league stats including average SOF from application query
const leagueStats = await leagueStatsQuery.execute({ leagueId });
if (leagueStats) {
setAverageSOF(leagueStats.averageSOF);
setCompletedRacesCount(leagueStats.completedRaces);
// Load league stats including average SOF from application use case
await leagueStatsUseCase.execute({ leagueId });
const leagueStatsViewModel = leagueStatsUseCase.presenter.getViewModel();
if (leagueStatsViewModel) {
setAverageSOF(leagueStatsViewModel.averageSOF);
setCompletedRacesCount(leagueStatsViewModel.completedRaces);
} else {
// Fallback: count completed races manually
const leagueRaces = await raceRepo.findByLeagueId(leagueId);

View File

@@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
import Card from '@/components/ui/Card';
import {
getLeagueRepository,
getGetLeagueScoringConfigQuery
getGetLeagueScoringConfigUseCase
} from '@/lib/di-container';
import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO';
import type { League } from '@gridpilot/racing/domain/entities/League';
@@ -25,7 +25,7 @@ export default function LeagueRulebookPage() {
async function loadData() {
try {
const leagueRepo = getLeagueRepository();
const scoringQuery = getGetLeagueScoringConfigQuery();
const scoringUseCase = getGetLeagueScoringConfigUseCase();
const leagueData = await leagueRepo.findById(leagueId);
if (!leagueData) {
@@ -35,8 +35,9 @@ export default function LeagueRulebookPage() {
setLeague(leagueData);
const scoring = await scoringQuery.execute({ leagueId });
setScoringConfig(scoring);
await scoringUseCase.execute({ leagueId });
const scoringViewModel = scoringUseCase.presenter.getViewModel();
setScoringConfig(scoringViewModel);
} catch (err) {
console.error('Failed to load scoring config:', err);
} finally {

View File

@@ -7,11 +7,11 @@ import Button from '@/components/ui/Button';
import {
getLeagueRepository,
getDriverRepository,
getGetLeagueFullConfigQuery,
getGetLeagueFullConfigUseCase,
getLeagueMembershipRepository,
getDriverStats,
getAllDriverRankings,
getListLeagueScoringPresetsQuery,
getListLeagueScoringPresetsUseCase,
getTransferLeagueOwnershipUseCase
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
@@ -59,8 +59,8 @@ export default function LeagueSettingsPage() {
try {
const leagueRepo = getLeagueRepository();
const driverRepo = getDriverRepository();
const query = getGetLeagueFullConfigQuery();
const presetsQuery = getListLeagueScoringPresetsQuery();
const useCase = getGetLeagueFullConfigUseCase();
const presetsUseCase = getListLeagueScoringPresetsUseCase();
const leagueData = await leagueRepo.findById(leagueId);
if (!leagueData) {
@@ -70,11 +70,13 @@ export default function LeagueSettingsPage() {
setLeague(leagueData);
const form = await query.execute({ leagueId });
setConfigForm(form);
await useCase.execute({ leagueId });
const configViewModel = useCase.presenter.getViewModel();
setConfigForm(configViewModel);
const presetsData = await presetsQuery.execute();
setPresets(presetsData);
await presetsUseCase.execute();
const presetsViewModel = presetsUseCase.presenter.getViewModel();
setPresets(presetsViewModel);
const entity = await driverRepo.findById(leagueData.ownerId);
if (entity) {

View File

@@ -10,7 +10,7 @@ import {
type LeagueDriverSeasonStatsDTO,
} from '@gridpilot/racing';
import {
getGetLeagueDriverSeasonStatsQuery,
getGetLeagueDriverSeasonStatsUseCase,
getDriverRepository,
getLeagueMembershipRepository
} from '@/lib/di-container';
@@ -32,12 +32,13 @@ export default function LeagueStandingsPage() {
const loadData = useCallback(async () => {
try {
const getLeagueDriverSeasonStatsQuery = getGetLeagueDriverSeasonStatsQuery();
const getLeagueDriverSeasonStatsUseCase = getGetLeagueDriverSeasonStatsUseCase();
const driverRepo = getDriverRepository();
const membershipRepo = getLeagueMembershipRepository();
const leagueStandings = await getLeagueDriverSeasonStatsQuery.execute({ leagueId });
setStandings(leagueStandings);
await getLeagueDriverSeasonStatsUseCase.execute({ leagueId });
const standingsViewModel = getLeagueDriverSeasonStatsUseCase.presenter.getViewModel();
setStandings(standingsViewModel);
const allDrivers = await driverRepo.findAll();
const driverDtos: DriverDTO[] = allDrivers

View File

@@ -31,7 +31,7 @@ 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 { getGetAllLeaguesWithCapacityAndScoringQuery } from '@/lib/di-container';
import { getGetAllLeaguesWithCapacityAndScoringUseCase } from '@/lib/di-container';
// ============================================================================
// TYPES
@@ -389,9 +389,10 @@ export default function LeaguesPage() {
const loadLeagues = async () => {
try {
const query = getGetAllLeaguesWithCapacityAndScoringQuery();
const allLeagues = await query.execute();
setRealLeagues(allLeagues);
const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
setRealLeagues(viewModel);
} catch (error) {
console.error('Failed to load leagues:', error);
} finally {

View File

@@ -38,11 +38,11 @@ import {
getDriverRepository,
getDriverStats,
getAllDriverRankings,
getGetDriverTeamQuery,
getGetDriverTeamUseCase,
getSocialRepository,
getImageService,
getGetAllTeamsQuery,
getGetTeamMembersQuery,
getGetAllTeamsUseCase,
getGetTeamMembersUseCase,
} from '@/lib/di-container';
import { Driver, EntityMappers, type DriverDTO, type Team } from '@gridpilot/racing';
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
@@ -381,18 +381,23 @@ export default function ProfilePage() {
setDriver(driverData);
// Load primary team data
const teamQuery = getGetDriverTeamQuery();
const teamResult = await teamQuery.execute({ driverId: currentDriverId });
setTeamData(teamResult);
const teamUseCase = getGetDriverTeamUseCase();
await teamUseCase.execute({ driverId: currentDriverId });
const teamViewModel = teamUseCase.presenter.getViewModel();
setTeamData(teamViewModel.result);
// Load ALL team memberships
const allTeamsQuery = getGetAllTeamsQuery();
const allTeams = await allTeamsQuery.execute();
const membershipsQuery = getGetTeamMembersQuery();
const allTeamsUseCase = getGetAllTeamsUseCase();
await allTeamsUseCase.execute();
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
const allTeams = allTeamsViewModel.teams;
const membershipsUseCase = getGetTeamMembersUseCase();
const memberships: TeamMembershipInfo[] = [];
for (const team of allTeams) {
const members = await membershipsQuery.execute({ teamId: team.id });
await membershipsUseCase.execute({ teamId: team.id });
const membersViewModel = membershipsUseCase.presenter.getViewModel();
const members = membersViewModel.members;
const membership = members.find((m) => m.driverId === currentDriverId);
if (membership) {
memberships.push({

View File

@@ -7,7 +7,7 @@ import Button from '@/components/ui/Button';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import PendingSponsorshipRequests, { type PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
import {
getGetPendingSponsorshipRequestsQuery,
getGetPendingSponsorshipRequestsUseCase,
getAcceptSponsorshipRequestUseCase,
getRejectSponsorshipRequestUseCase,
getDriverRepository,
@@ -46,7 +46,7 @@ export default function SponsorshipRequestsPage() {
const teamRepo = getTeamRepository();
const leagueMembershipRepo = getLeagueMembershipRepository();
const teamMembershipRepo = getTeamMembershipRepository();
const query = getGetPendingSponsorshipRequestsQuery();
const useCase = getGetPendingSponsorshipRequestsUseCase();
const allSections: EntitySection[] = [];

View File

@@ -17,11 +17,11 @@ import {
getRaceRepository,
getLeagueRepository,
getDriverRepository,
getGetRaceRegistrationsQuery,
getIsDriverRegisteredForRaceQuery,
getGetRaceRegistrationsUseCase,
getIsDriverRegisteredForRaceUseCase,
getRegisterForRaceUseCase,
getWithdrawFromRaceUseCase,
getGetRaceWithSOFQuery,
getGetRaceWithSOFUseCase,
getResultRepository,
getImageService,
} from '@/lib/di-container';
@@ -80,7 +80,7 @@ export default function RaceDetailPage() {
try {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
const raceWithSOFQuery = getGetRaceWithSOFQuery();
const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
const raceData = await raceRepo.findById(raceId);
@@ -92,10 +92,11 @@ export default function RaceDetailPage() {
setRace(raceData);
// Load race with SOF from application query
const raceWithSOF = await raceWithSOFQuery.execute({ raceId });
if (raceWithSOF) {
setRaceSOF(raceWithSOF.strengthOfField);
// Load race with SOF from application use case
await raceWithSOFUseCase.execute({ raceId });
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel();
if (raceViewModel) {
setRaceSOF(raceViewModel.strengthOfField);
}
// Load league data
@@ -135,8 +136,10 @@ export default function RaceDetailPage() {
try {
const driverRepo = getDriverRepository();
const raceRegistrationsQuery = getGetRaceRegistrationsQuery();
const registeredDriverIds = await raceRegistrationsQuery.execute({ raceId });
const raceRegistrationsUseCase = getGetRaceRegistrationsUseCase();
await raceRegistrationsUseCase.execute({ raceId });
const registrationsViewModel = raceRegistrationsUseCase.presenter.getViewModel();
const registeredDriverIds = registrationsViewModel.registeredDriverIds;
const drivers = await Promise.all(
registeredDriverIds.map((id: string) => driverRepo.findById(id)),
@@ -144,12 +147,13 @@ export default function RaceDetailPage() {
const validDrivers = drivers.filter((d: Driver | null): d is Driver => d !== null);
setEntryList(validDrivers);
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
const userIsRegistered = await isRegisteredQuery.execute({
const isRegisteredUseCase = getIsDriverRegisteredForRaceUseCase();
await isRegisteredUseCase.execute({
raceId,
driverId: currentDriverId,
});
setIsUserRegistered(userIsRegistered);
const registrationViewModel = isRegisteredUseCase.presenter.getViewModel();
setIsUserRegistered(registrationViewModel.isRegistered);
const membership = getMembership(leagueId, currentDriverId);
const isUpcoming = race?.status === 'scheduled';

View File

@@ -18,8 +18,8 @@ import {
getResultRepository,
getStandingRepository,
getDriverRepository,
getGetRaceWithSOFQuery,
getGetRacePenaltiesQuery,
getGetRaceWithSOFUseCase,
getGetRacePenaltiesUseCase,
} from '@/lib/di-container';
interface PenaltyData {
@@ -52,7 +52,7 @@ export default function RaceResultsPage() {
const leagueRepo = getLeagueRepository();
const resultRepo = getResultRepository();
const driverRepo = getDriverRepository();
const raceWithSOFQuery = getGetRaceWithSOFQuery();
const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
const raceData = await raceRepo.findById(raceId);
@@ -64,10 +64,11 @@ export default function RaceResultsPage() {
setRace(raceData);
// Load race with SOF from application query
const raceWithSOF = await raceWithSOFQuery.execute({ raceId });
if (raceWithSOF) {
setRaceSOF(raceWithSOF.strengthOfField);
// Load race with SOF from application use case
await raceWithSOFUseCase.execute({ raceId });
const raceViewModel = raceWithSOFUseCase.presenter.getViewModel();
if (raceViewModel) {
setRaceSOF(raceViewModel.strengthOfField);
}
// Load league data
@@ -89,10 +90,11 @@ export default function RaceResultsPage() {
// Load penalties for this race
try {
const penaltiesQuery = getGetRacePenaltiesQuery();
const penaltiesData = await penaltiesQuery.execute(raceId);
const penaltiesUseCase = getGetRacePenaltiesUseCase();
await penaltiesUseCase.execute(raceId);
const penaltiesViewModel = penaltiesUseCase.presenter.getViewModel();
// Map the DTO to the PenaltyData interface expected by ResultsTable
setPenalties(penaltiesData.map(p => ({
setPenalties(penaltiesViewModel.map(p => ({
driverId: p.driverId,
type: p.type,
value: p.value,

View File

@@ -7,8 +7,7 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
import { League } from '@gridpilot/racing/domain/entities/League';
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
import { getGetRacesPageDataUseCase } from '@/lib/di-container';
import {
Calendar,
Clock,
@@ -32,8 +31,13 @@ type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
export default function RacesPage() {
const router = useRouter();
const [races, setRaces] = useState<Race[]>([]);
const [leagues, setLeagues] = useState<Map<string, League>>(new Map());
const [pageData, setPageData] = useState<{
races: Array<{ race: Race; leagueName: string }>;
stats: { total: number; scheduled: number; running: number; completed: number };
liveRaces: Array<{ race: Race; leagueName: string }>;
upcomingRaces: Array<{ race: Race; leagueName: string }>;
recentResults: Array<{ race: Race; leagueName: string }>;
} | null>(null);
const [loading, setLoading] = useState(true);
// Filters
@@ -43,19 +47,74 @@ export default function RacesPage() {
const loadRaces = async () => {
try {
const raceRepo = getRaceRepository();
const leagueRepo = getLeagueRepository();
const [allRaces, allLeagues] = await Promise.all([
raceRepo.findAll(),
leagueRepo.findAll()
]);
setRaces(allRaces.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()));
const useCase = getGetRacesPageDataUseCase();
await useCase.execute();
const data = useCase.presenter.getViewModel();
const leagueMap = new Map<string, League>();
allLeagues.forEach(league => leagueMap.set(league.id, league));
setLeagues(leagueMap);
// Transform ViewModel back to page format
setPageData({
races: data.races.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
stats: data.stats,
liveRaces: data.liveRaces.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
upcomingRaces: data.upcomingThisWeek.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
recentResults: data.recentResults.map(r => ({
race: {
id: r.id,
track: r.track,
car: r.car,
scheduledAt: new Date(r.scheduledAt),
status: r.status,
leagueId: r.leagueId,
strengthOfField: r.strengthOfField,
isUpcoming: () => r.isUpcoming,
isLive: () => r.isLive,
isPast: () => r.isPast,
} as Race,
leagueName: r.leagueName,
})),
});
} catch (err) {
console.error('Failed to load races:', err);
} finally {
@@ -69,7 +128,9 @@ export default function RacesPage() {
// Filter races
const filteredRaces = useMemo(() => {
return races.filter(race => {
if (!pageData) return [];
return pageData.races.filter(({ race }) => {
// Status filter
if (statusFilter !== 'all' && race.status !== statusFilter) {
return false;
@@ -93,53 +154,25 @@ export default function RacesPage() {
return true;
});
}, [races, statusFilter, leagueFilter, timeFilter]);
}, [pageData, statusFilter, leagueFilter, timeFilter]);
// Group races by date for calendar view
const racesByDate = useMemo(() => {
const grouped = new Map<string, Race[]>();
filteredRaces.forEach(race => {
const dateKey = race.scheduledAt.toISOString().split('T')[0];
const grouped = new Map<string, Array<{ race: Race; leagueName: string }>>();
filteredRaces.forEach(item => {
const dateKey = item.race.scheduledAt.toISOString().split('T')[0];
if (!grouped.has(dateKey)) {
grouped.set(dateKey, []);
}
grouped.get(dateKey)!.push(race);
grouped.get(dateKey)!.push(item);
});
return grouped;
}, [filteredRaces]);
// Get upcoming races (next 7 days)
const upcomingRaces = useMemo(() => {
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
return races.filter(race =>
race.isUpcoming() &&
race.scheduledAt >= now &&
race.scheduledAt <= nextWeek
).slice(0, 5);
}, [races]);
// Get live races
const liveRaces = useMemo(() => {
return races.filter(race => race.isLive());
}, [races]);
// Get recent results
const recentResults = useMemo(() => {
return races
.filter(race => race.status === 'completed')
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime())
.slice(0, 3);
}, [races]);
// Stats
const stats = useMemo(() => {
const total = races.length;
const scheduled = races.filter(r => r.status === 'scheduled').length;
const running = races.filter(r => r.status === 'running').length;
const completed = races.filter(r => r.status === 'completed').length;
return { total, scheduled, running, completed };
}, [races]);
const upcomingRaces = pageData?.upcomingRaces ?? [];
const liveRaces = pageData?.liveRaces ?? [];
const recentResults = pageData?.recentResults ?? [];
const stats = pageData?.stats ?? { total: 0, scheduled: 0, running: 0, completed: 0 };
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
@@ -298,8 +331,8 @@ export default function RacesPage() {
</div>
<div className="space-y-3">
{liveRaces.map(race => (
<div
{liveRaces.map(({ race, leagueName }) => (
<div
key={race.id}
onClick={() => router.push(`/races/${race.id}`)}
className="flex items-center justify-between p-4 bg-deep-graphite/80 rounded-lg border border-performance-green/20 cursor-pointer hover:border-performance-green/40 transition-all"
@@ -310,7 +343,7 @@ export default function RacesPage() {
</div>
<div>
<h3 className="font-semibold text-white">{race.track}</h3>
<p className="text-sm text-gray-400">{leagues.get(race.leagueId)?.name}</p>
<p className="text-sm text-gray-400">{leagueName}</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400" />
@@ -352,11 +385,14 @@ export default function RacesPage() {
className="px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="all">All Leagues</option>
{Array.from(leagues.values()).map(league => (
<option key={league.id} value={league.id}>
{league.name}
</option>
))}
{pageData && [...new Set(pageData.races.map(r => r.race.leagueId))].map(leagueId => {
const item = pageData.races.find(r => r.race.leagueId === leagueId);
return item ? (
<option key={leagueId} value={leagueId}>
{item.leagueName}
</option>
) : null;
})}
</select>
</div>
</Card>
@@ -371,7 +407,7 @@ export default function RacesPage() {
<div>
<p className="text-white font-medium mb-1">No races found</p>
<p className="text-sm text-gray-500">
{races.length === 0
{pageData?.races.length === 0
? 'No races have been scheduled yet'
: 'Try adjusting your filters'}
</p>
@@ -397,10 +433,9 @@ export default function RacesPage() {
{/* Races for this date */}
<div className="space-y-2">
{dayRaces.map(race => {
{dayRaces.map(({ race, leagueName }) => {
const config = statusConfig[race.status];
const StatusIcon = config.icon;
const league = leagues.get(race.leagueId);
return (
<div
@@ -456,19 +491,17 @@ export default function RacesPage() {
</div>
{/* League Link */}
{league && (
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
<Link
href={`/leagues/${league.id}`}
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
>
<Trophy className="w-3.5 h-3.5" />
{league.name}
<ArrowRight className="w-3 h-3" />
</Link>
</div>
)}
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
<Link
href={`/leagues/${race.leagueId}`}
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
>
<Trophy className="w-3.5 h-3.5" />
{leagueName}
<ArrowRight className="w-3 h-3" />
</Link>
</div>
</div>
{/* Arrow */}
@@ -515,8 +548,8 @@ export default function RacesPage() {
</p>
) : (
<div className="space-y-3">
{upcomingRaces.map((race, index) => (
<div
{upcomingRaces.map(({ race }) => (
<div
key={race.id}
onClick={() => router.push(`/races/${race.id}`)}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"
@@ -552,8 +585,8 @@ export default function RacesPage() {
</p>
) : (
<div className="space-y-3">
{recentResults.map(race => (
<div
{recentResults.map(({ race }) => (
<div
key={race.id}
onClick={() => router.push(`/races/${race.id}/results`)}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite cursor-pointer transition-colors"

View File

@@ -32,57 +32,73 @@ interface Sponsorship {
drivers: number;
}
// Mock data - in production would come from repository
const MOCK_SPONSORSHIPS: Sponsorship[] = [
{
id: 'sp-1',
leagueId: 'league-1',
leagueName: 'GT3 Pro Championship',
tier: 'main',
status: 'active',
startDate: new Date('2025-01-01'),
endDate: new Date('2025-06-30'),
price: 1200,
impressions: 45200,
drivers: 32,
},
{
id: 'sp-2',
leagueId: 'league-2',
leagueName: 'Endurance Masters',
tier: 'main',
status: 'active',
startDate: new Date('2025-02-01'),
endDate: new Date('2025-07-31'),
price: 1000,
impressions: 38100,
drivers: 48,
},
{
id: 'sp-3',
leagueId: 'league-3',
leagueName: 'Formula Sim Series',
tier: 'secondary',
status: 'active',
startDate: new Date('2025-03-01'),
endDate: new Date('2025-08-31'),
price: 400,
impressions: 22800,
drivers: 24,
},
{
id: 'sp-4',
leagueId: 'league-4',
leagueName: 'Touring Car Cup',
tier: 'secondary',
status: 'pending',
startDate: new Date('2025-04-01'),
endDate: new Date('2025-09-30'),
price: 350,
impressions: 0,
drivers: 28,
},
];
interface SponsorshipDetailApi {
id: string;
leagueId: string;
leagueName: string;
seasonId: string;
seasonName: string;
seasonStartDate?: string;
seasonEndDate?: string;
tier: 'main' | 'secondary';
status: string;
pricing: {
amount: number;
currency: string;
};
metrics: {
drivers: number;
races: number;
completedRaces: number;
impressions: number;
};
createdAt: string;
activatedAt?: string;
}
interface SponsorSponsorshipsResponse {
sponsorId: string;
sponsorName: string;
sponsorships: SponsorshipDetailApi[];
summary: {
totalSponsorships: number;
activeSponsorships: number;
totalInvestment: number;
totalPlatformFees: number;
currency: string;
};
}
function mapSponsorshipStatus(status: string): 'active' | 'pending' | 'expired' {
switch (status) {
case 'active':
return 'active';
case 'pending':
return 'pending';
default:
return 'expired';
}
}
function mapApiToSponsorships(response: SponsorSponsorshipsResponse): Sponsorship[] {
return response.sponsorships.map((s) => {
const start = s.seasonStartDate ? new Date(s.seasonStartDate) : new Date(s.createdAt);
const end = s.seasonEndDate ? new Date(s.seasonEndDate) : start;
return {
id: s.id,
leagueId: s.leagueId,
leagueName: s.leagueName,
tier: s.tier,
status: mapSponsorshipStatus(s.status),
startDate: start,
endDate: end,
price: s.pricing.amount,
impressions: s.metrics.impressions,
drivers: s.metrics.drivers,
};
});
}
function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
const router = useRouter();
@@ -179,18 +195,59 @@ function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
export default function SponsorCampaignsPage() {
const router = useRouter();
const [filter, setFilter] = useState<'all' | 'active' | 'pending' | 'expired'>('all');
const filteredSponsorships = filter === 'all'
? MOCK_SPONSORSHIPS
: MOCK_SPONSORSHIPS.filter(s => s.status === filter);
const [sponsorships, setSponsorships] = useState<Sponsorship[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
async function fetchSponsorships() {
try {
const response = await fetch('/api/sponsors/sponsorships');
if (!response.ok) {
if (!isMounted) return;
setSponsorships([]);
return;
}
const json: SponsorSponsorshipsResponse = await response.json();
if (!isMounted) return;
setSponsorships(mapApiToSponsorships(json));
} catch {
if (!isMounted) return;
setSponsorships([]);
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchSponsorships();
return () => {
isMounted = false;
};
}, []);
const filteredSponsorships = filter === 'all'
? sponsorships
: sponsorships.filter(s => s.status === filter);
const stats = {
total: MOCK_SPONSORSHIPS.length,
active: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').length,
pending: MOCK_SPONSORSHIPS.filter(s => s.status === 'pending').length,
totalInvestment: MOCK_SPONSORSHIPS.reduce((sum, s) => sum + s.price, 0),
total: sponsorships.length,
active: sponsorships.filter(s => s.status === 'active').length,
pending: sponsorships.filter(s => s.status === 'pending').length,
totalInvestment: sponsorships.reduce((sum, s) => sum + s.price, 0),
};
if (loading) {
return (
<div className="max-w-6xl mx-auto py-8 px-4">
<p className="text-gray-400">Loading sponsorships</p>
</div>
);
}
return (
<div className="max-w-6xl mx-auto py-8 px-4">
{/* Header */}

View File

@@ -52,64 +52,6 @@ interface SponsorDashboardData {
};
}
// Fallback mock data for demo mode
const MOCK_DASHBOARD: SponsorDashboardData = {
sponsorId: 'demo-sponsor',
sponsorName: 'Demo Sponsor',
metrics: {
impressions: 124500,
impressionsChange: 12.5,
uniqueViewers: 8420,
viewersChange: 8.3,
races: 24,
drivers: 156,
exposure: 87.5,
exposureChange: 5.2,
},
sponsoredLeagues: [
{
id: 'league-1',
name: 'GT3 Pro Championship',
tier: 'main',
drivers: 32,
races: 12,
impressions: 45200,
status: 'active',
},
{
id: 'league-2',
name: 'Endurance Masters',
tier: 'main',
drivers: 48,
races: 6,
impressions: 38100,
status: 'active',
},
{
id: 'league-3',
name: 'Formula Sim Series',
tier: 'secondary',
drivers: 24,
races: 8,
impressions: 22800,
status: 'active',
},
{
id: 'league-4',
name: 'Touring Car Cup',
tier: 'secondary',
drivers: 28,
races: 10,
impressions: 18400,
status: 'upcoming',
},
],
investment: {
activeSponsorships: 4,
totalInvestment: 2400,
costPerThousandViews: 19.28,
},
};
function MetricCard({
title,
@@ -205,15 +147,13 @@ export default function SponsorDashboardPage() {
try {
const response = await fetch('/api/sponsors/dashboard');
if (response.ok) {
const dashboardData = await response.json();
const dashboardData: SponsorDashboardData = await response.json();
setData(dashboardData);
} else {
// Use mock data for demo mode
setData(MOCK_DASHBOARD);
setError('Failed to load sponsor dashboard');
}
} catch {
// Use mock data on error
setData(MOCK_DASHBOARD);
setError('Failed to load sponsor dashboard');
} finally {
setLoading(false);
}
@@ -230,7 +170,19 @@ export default function SponsorDashboardPage() {
);
}
const dashboardData = data || MOCK_DASHBOARD;
if (!data) {
return (
<div className="max-w-7xl mx-auto py-8 px-4">
<div className="rounded-lg bg-warning-amber/10 border border-warning-amber/30 p-4">
<p className="text-sm text-warning-amber">
{error ?? 'No sponsor dashboard data available yet.'}
</p>
</div>
</div>
);
}
const dashboardData = data;
return (
<div className="max-w-7xl mx-auto py-8 px-4">

View File

@@ -14,8 +14,8 @@ import TeamStandings from '@/components/teams/TeamStandings';
import TeamAdmin from '@/components/teams/TeamAdmin';
import JoinTeamButton from '@/components/teams/JoinTeamButton';
import {
getGetTeamDetailsQuery,
getGetTeamMembersQuery,
getGetTeamDetailsUseCase,
getGetTeamMembersUseCase,
getTeamMembershipRepository,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
@@ -39,11 +39,15 @@ export default function TeamDetailPage() {
const loadTeamData = useCallback(async () => {
setLoading(true);
try {
const detailsQuery = getGetTeamDetailsQuery();
const membersQuery = getGetTeamMembersQuery();
const detailsUseCase = getGetTeamDetailsUseCase();
const membersUseCase = getGetTeamMembersUseCase();
const details = await detailsQuery.execute({ teamId, driverId: currentDriverId });
const teamMemberships = await membersQuery.execute({ teamId });
await detailsUseCase.execute({ teamId, driverId: currentDriverId });
const detailsViewModel = detailsUseCase.presenter.getViewModel();
await membersUseCase.execute({ teamId });
const membersViewModel = membersUseCase.presenter.getViewModel();
const teamMemberships = membersViewModel.members;
const adminStatus =
teamMemberships.some(
@@ -52,7 +56,7 @@ export default function TeamDetailPage() {
(m.role === 'owner' || m.role === 'manager'),
) ?? false;
setTeam(details.team);
setTeam(detailsViewModel.team);
setMemberships(teamMemberships);
setIsAdmin(adminStatus);
} finally {

View File

@@ -22,7 +22,7 @@ import {
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import { getGetAllTeamsQuery, getGetTeamMembersQuery, getDriverStats } from '@/lib/di-container';
import { getGetAllTeamsUseCase, getGetTeamMembersUseCase, getDriverStats } from '@/lib/di-container';
import type { Team } from '@gridpilot/racing';
// ============================================================================
@@ -260,15 +260,19 @@ export default function TeamLeaderboardPage() {
const loadTeams = async () => {
try {
const allTeamsQuery = getGetAllTeamsQuery();
const teamMembersQuery = getGetTeamMembersQuery();
const allTeamsUseCase = getGetAllTeamsUseCase();
const teamMembersUseCase = getGetTeamMembersUseCase();
const allTeams = await allTeamsQuery.execute();
await allTeamsUseCase.execute();
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
const allTeams = allTeamsViewModel.teams;
const teamData: TeamDisplayData[] = [];
await Promise.all(
allTeams.map(async (team: Team) => {
const memberships = await teamMembersQuery.execute({ teamId: team.id });
await teamMembersUseCase.execute({ teamId: team.id });
const membershipsViewModel = teamMembersUseCase.presenter.getViewModel();
const memberships = membershipsViewModel.members;
const memberCount = memberships.length;
let ratingSum = 0;

View File

@@ -28,30 +28,14 @@ import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading';
import CreateTeamForm from '@/components/teams/CreateTeamForm';
import { getGetAllTeamsQuery, getGetTeamMembersQuery, getDriverStats } from '@/lib/di-container';
import type { Team } from '@gridpilot/racing';
import { getGetTeamsLeaderboardUseCase } from '@/lib/di-container';
import type { TeamLeaderboardItemViewModel, SkillLevel } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
interface TeamDisplayData {
id: string;
name: string;
memberCount: number;
rating: number | null;
totalWins: number;
totalRaces: number;
performanceLevel: SkillLevel;
isRecruiting: boolean;
createdAt: Date;
description?: string;
specialization?: 'endurance' | 'sprint' | 'mixed';
region?: string;
languages?: string[];
}
type TeamDisplayData = TeamLeaderboardItemViewModel;
// ============================================================================
// SKILL LEVEL CONFIG
@@ -463,59 +447,10 @@ export default function TeamsPage() {
const loadTeams = async () => {
try {
const allTeamsQuery = getGetAllTeamsQuery();
const teamMembersQuery = getGetTeamMembersQuery();
const allTeams = await allTeamsQuery.execute();
const teamData: TeamDisplayData[] = [];
await Promise.all(
allTeams.map(async (team: Team) => {
const memberships = await teamMembersQuery.execute({ teamId: team.id });
const memberCount = memberships.length;
let ratingSum = 0;
let ratingCount = 0;
let totalWins = 0;
let totalRaces = 0;
for (const membership of memberships) {
const stats = getDriverStats(membership.driverId);
if (!stats) continue;
if (typeof stats.rating === 'number') {
ratingSum += stats.rating;
ratingCount += 1;
}
totalWins += stats.wins ?? 0;
totalRaces += stats.totalRaces ?? 0;
}
const averageRating = ratingCount > 0 ? ratingSum / ratingCount : null;
let performanceLevel: TeamDisplayData['performanceLevel'] = 'beginner';
if (averageRating !== null) {
if (averageRating >= 4500) performanceLevel = 'pro';
else if (averageRating >= 3000) performanceLevel = 'advanced';
else if (averageRating >= 2000) performanceLevel = 'intermediate';
}
teamData.push({
id: team.id,
name: team.name,
memberCount,
rating: averageRating,
totalWins,
totalRaces,
performanceLevel,
isRecruiting: true,
createdAt: new Date(),
});
}),
);
setRealTeams(teamData);
const useCase = getGetTeamsLeaderboardUseCase();
await useCase.execute();
const viewModel = useCase.presenter.getViewModel();
setRealTeams(viewModel.teams);
} catch (error) {
console.error('Failed to load teams:', error);
} finally {