wip
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user