refactor page to use services

This commit is contained in:
2025-12-18 17:02:48 +01:00
parent fc386db06a
commit 9814d9682c
27 changed files with 434 additions and 282 deletions

View File

@@ -24,8 +24,7 @@ import {
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
// Dashboard service imports import { useServices } from '@/lib/services/ServiceProvider';
import { ServiceFactory } from '@/lib/services/ServiceFactory';
import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel'; import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
@@ -81,27 +80,26 @@ function getGreeting(): string {
import { DashboardFeedItemSummaryViewModel } from '@/lib/view-models/DashboardOverviewViewModel'; import { DashboardFeedItemSummaryViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
export default function DashboardPage() { export default function DashboardPage() {
const [dashboardData, setDashboardData] = useState<DashboardOverviewViewModel | null>(null); const { dashboardService } = useServices();
const [isLoading, setIsLoading] = useState(true); const [dashboardData, setDashboardData] = useState<DashboardOverviewViewModel | null>(null);
const [error, setError] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchDashboardData = async () => { const fetchDashboardData = async () => {
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'); const data = await dashboardService.getDashboardOverview();
const dashboardService = serviceFactory.createDashboardService(); setDashboardData(data);
const data = await dashboardService.getDashboardOverview(); } catch (err) {
setDashboardData(data); console.error('Failed to fetch dashboard data:', err);
} catch (err) { setError('Failed to load dashboard data');
console.error('Failed to fetch dashboard data:', err); } finally {
setError('Failed to load dashboard data'); setIsLoading(false);
} finally { }
setIsLoading(false); };
}
};
fetchDashboardData(); fetchDashboardData();
}, []); }, [dashboardService]);
if (isLoading) { if (isLoading) {
return ( return (

View File

@@ -36,7 +36,7 @@ import {
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Breadcrumbs from '@/components/layout/Breadcrumbs'; import Breadcrumbs from '@/components/layout/Breadcrumbs';
import { ServiceFactory } from '@/lib/services/ServiceFactory'; import { useServices } from '@/lib/services/ServiceProvider';
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel'; import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
// ============================================================================ // ============================================================================
@@ -314,6 +314,7 @@ export default function DriverDetailPage() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const driverId = params.id as string; const driverId = params.id as string;
const { driverService, teamService } = useServices();
const [driverProfile, setDriverProfile] = useState<DriverProfileViewModel | null>(null); const [driverProfile, setDriverProfile] = useState<DriverProfileViewModel | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -354,11 +355,6 @@ export default function DriverDetailPage() {
const loadDriver = async () => { const loadDriver = async () => {
try { try {
// Initialize service factory
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || '');
const driverService = serviceFactory.createDriverService();
const teamService = serviceFactory.createTeamService();
// Get driver profile // Get driver profile
const profileViewModel = await driverService.getDriverProfile(driverId); const profileViewModel = await driverService.getDriverProfile(driverId);

View File

@@ -27,7 +27,7 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { getDriverLeaderboard } from '@/lib/services/drivers/DriverService'; import { useServices } from '@/lib/services/ServiceProvider';
import type { DriverLeaderboardViewModel } from '@/lib/view-models'; import type { DriverLeaderboardViewModel } from '@/lib/view-models';
import Image from 'next/image'; import Image from 'next/image';
@@ -373,6 +373,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
export default function DriversPage() { export default function DriversPage() {
const router = useRouter(); const router = useRouter();
const { driverService } = useServices();
const [drivers, setDrivers] = useState<DriverLeaderboardItemViewModel[]>([]); const [drivers, setDrivers] = useState<DriverLeaderboardItemViewModel[]>([]);
const [viewModel, setViewModel] = useState<DriverLeaderboardViewModel | null>(null); const [viewModel, setViewModel] = useState<DriverLeaderboardViewModel | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -383,7 +384,7 @@ export default function DriversPage() {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
const vm = await getDriverLeaderboard(); const vm = await driverService.getDriverLeaderboard();
setViewModel(vm); setViewModel(vm);
setDrivers(vm.drivers); setDrivers(vm.drivers);
setTotalRaces(vm.totalRaces); setTotalRaces(vm.totalRaces);
@@ -393,7 +394,7 @@ export default function DriversPage() {
}; };
void load(); void load();
}, []); }, [driverService]);
const handleDriverClick = (driverId: string) => { const handleDriverClick = (driverId: string) => {
router.push(`/drivers/${driverId}`); router.push(`/drivers/${driverId}`);

View File

@@ -10,6 +10,7 @@ import AlphaFooter from '@/components/alpha/AlphaFooter';
import { AuthProvider } from '@/lib/auth/AuthContext'; import { AuthProvider } from '@/lib/auth/AuthContext';
import NotificationProvider from '@/components/notifications/NotificationProvider'; import NotificationProvider from '@/components/notifications/NotificationProvider';
import DevToolbar from '@/components/dev/DevToolbar'; import DevToolbar from '@/components/dev/DevToolbar';
import { ServiceProvider } from '@/lib/services/ServiceProvider';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -60,17 +61,19 @@ export default async function RootLayout({
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
</head> </head>
<body className="antialiased overflow-x-hidden min-h-screen bg-deep-graphite flex flex-col"> <body className="antialiased overflow-x-hidden min-h-screen bg-deep-graphite flex flex-col">
<AuthProvider initialSession={session}> <ServiceProvider>
<NotificationProvider> <AuthProvider initialSession={session}>
<AlphaNav /> <NotificationProvider>
<AlphaBanner /> <AlphaNav />
<main className="flex-1 max-w-7xl mx-auto px-6 py-8 w-full"> <AlphaBanner />
{children} <main className="flex-1 max-w-7xl mx-auto px-6 py-8 w-full">
</main> {children}
<AlphaFooter /> </main>
<DevToolbar /> <AlphaFooter />
</NotificationProvider> <DevToolbar />
</AuthProvider> </NotificationProvider>
</AuthProvider>
</ServiceProvider>
</body> </body>
</html> </html>
); );

View File

@@ -19,14 +19,16 @@ import {
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@core/racing/application/presenters/IDriversLeaderboardPresenter'; import { useServices } from '@/lib/services/ServiceProvider';
import Image from 'next/image'; import Image from 'next/image';
// ============================================================================ // ============================================================================
// TYPES // TYPES
// ============================================================================ // ============================================================================
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate'; type SortBy = 'rank' | 'rating' | 'wins' | 'podiums' | 'winRate';
type DriverListItem = DriverLeaderboardItemViewModel; type DriverListItem = DriverLeaderboardItemViewModel;
@@ -180,13 +182,9 @@ export default function DriverLeaderboardPage() {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
const useCase = getGetDriversLeaderboardUseCase(); const { driverService } = useServices();
const presenter = new DriversLeaderboardPresenter(); const viewModel = await driverService.getDriverLeaderboard();
await useCase.execute(undefined as void, presenter); setDrivers(viewModel.drivers);
const viewModel = presenter.getViewModel();
if (viewModel) {
setDrivers(viewModel.drivers);
}
setLoading(false); setLoading(false);
}; };

View File

@@ -20,19 +20,20 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter'; import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
import { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter'; import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import type { DriverLeaderboardItemViewModel, SkillLevel } from '@core/racing/application/presenters/IDriversLeaderboardPresenter'; import { useServices } from '@/lib/services/ServiceProvider';
import type { TeamLeaderboardItemViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
import Image from 'next/image'; import Image from 'next/image';
// ============================================================================ // ============================================================================
// TYPES // TYPES
// ============================================================================ // ============================================================================
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type DriverListItem = DriverLeaderboardItemViewModel; type DriverListItem = DriverLeaderboardItemViewModel;
type TeamDisplayData = TeamLeaderboardItemViewModel; type TeamDisplayData = TeamSummaryViewModel;
// ============================================================================ // ============================================================================
// SKILL LEVEL CONFIG // SKILL LEVEL CONFIG
@@ -285,19 +286,12 @@ export default function LeaderboardsPage() {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
try { try {
const driversUseCase = getGetDriversLeaderboardUseCase(); const { driverService, teamService } = useServices();
const teamsUseCase = getGetTeamsLeaderboardUseCase(); const driversViewModel = await driverService.getDriverLeaderboard();
const driversPresenter = new DriversLeaderboardPresenter(); const teams = await teamService.getAllTeams();
const teamsPresenter = new TeamsLeaderboardPresenter();
await driversUseCase.execute(undefined as void, driversPresenter); setDrivers(driversViewModel.drivers);
await teamsUseCase.execute(undefined as void, teamsPresenter); setTeams(teams);
const driversViewModel = driversPresenter.getViewModel();
const teamsViewModel = teamsPresenter.getViewModel();
setDrivers(driversViewModel?.drivers ?? []);
setTeams(teamsViewModel ? teamsViewModel.teams : []);
} catch (error) { } catch (error) {
console.error('Failed to load leaderboard data:', error); console.error('Failed to load leaderboard data:', error);
setDrivers([]); setDrivers([]);

View File

@@ -3,7 +3,7 @@
import Breadcrumbs from '@/components/layout/Breadcrumbs'; import Breadcrumbs from '@/components/layout/Breadcrumbs';
import LeagueHeader from '@/components/leagues/LeagueHeader'; import LeagueHeader from '@/components/leagues/LeagueHeader';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { ServiceFactory } from '@/lib/services/ServiceFactory'; import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueDetailViewModel } from '@/lib/view-models/LeagueDetailViewModel'; import { LeagueDetailViewModel } from '@/lib/view-models/LeagueDetailViewModel';
import { useParams, usePathname, useRouter } from 'next/navigation'; import { useParams, usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -18,6 +18,7 @@ export default function LeagueLayout({
const router = useRouter(); const router = useRouter();
const leagueId = params.id as string; const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();
const { leagueService } = useServices();
const [leagueDetail, setLeagueDetail] = useState<LeagueDetailViewModel | null>(null); const [leagueDetail, setLeagueDetail] = useState<LeagueDetailViewModel | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -25,9 +26,6 @@ export default function LeagueLayout({
useEffect(() => { useEffect(() => {
async function loadLeague() { async function loadLeague() {
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || '');
const leagueService = serviceFactory.createLeagueService();
const leagueDetailData = await leagueService.getLeagueDetail(leagueId, currentDriverId); const leagueDetailData = await leagueService.getLeagueDetail(leagueId, currentDriverId);
setLeagueDetail(leagueDetailData); setLeagueDetail(leagueDetailData);
@@ -39,7 +37,7 @@ export default function LeagueLayout({
} }
loadLeague(); loadLeague();
}, [leagueId, currentDriverId]); }, [leagueId, currentDriverId, leagueService]);
if (loading) { if (loading) {
return ( return (

View File

@@ -13,9 +13,8 @@ import SponsorInsightsCard, {
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
import { LeagueRoleDisplay } from '@/lib/display-objects/LeagueRoleDisplay'; import { LeagueRoleDisplay } from '@/lib/display-objects/LeagueRoleDisplay';
import { ServiceFactory } from '@/lib/services/ServiceFactory'; import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueDetailPageViewModel } from '@/lib/view-models/LeagueDetailPageViewModel'; import { LeagueDetailPageViewModel } from '@/lib/view-models/LeagueDetailPageViewModel';
import { Calendar, ExternalLink, Star, Trophy, Users } from 'lucide-react'; import { Calendar, ExternalLink, Star, Trophy, Users } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -26,6 +25,7 @@ export default function LeagueDetailPage() {
const params = useParams(); const params = useParams();
const leagueId = params.id as string; const leagueId = params.id as string;
const isSponsor = useSponsorMode(); const isSponsor = useSponsorMode();
const { leagueService, leagueMembershipService } = useServices();
const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null); const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -33,8 +33,8 @@ export default function LeagueDetailPage() {
const [endRaceModalRaceId, setEndRaceModalRaceId] = useState<string | null>(null); const [endRaceModalRaceId, setEndRaceModalRaceId] = useState<string | null>(null);
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();
const membership = LeagueMembershipService.getMembership(leagueId, currentDriverId); const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
const leagueMemberships = LeagueMembershipService.getLeagueMembers(leagueId); const leagueMemberships = leagueMembershipService.getLeagueMembers(leagueId);
// Build metrics for SponsorInsightsCard // Build metrics for SponsorInsightsCard
const leagueMetrics: SponsorMetric[] = useMemo(() => { const leagueMetrics: SponsorMetric[] = useMemo(() => {
@@ -49,9 +49,6 @@ export default function LeagueDetailPage() {
const loadLeagueData = async () => { const loadLeagueData = async () => {
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_URL || '');
const leagueService = serviceFactory.createLeagueService();
const viewModelData = await leagueService.getLeagueDetailPageData(leagueId); const viewModelData = await leagueService.getLeagueDetailPageData(leagueId);
if (!viewModelData) { if (!viewModelData) {
@@ -265,9 +262,9 @@ export default function LeagueDetailPage() {
{viewModel.socialLinks && ( {viewModel.socialLinks && (
<div className="mt-4 pt-4 border-t border-charcoal-outline"> <div className="mt-4 pt-4 border-t border-charcoal-outline">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{league.socialLinks.discordUrl && ( {viewModel.socialLinks.discordUrl && (
<a <a
href={league.socialLinks.discordUrl} href={viewModel.socialLinks.discordUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="inline-flex items-center gap-1 rounded-full border border-primary-blue/40 bg-primary-blue/10 px-2 py-1 text-xs text-primary-blue hover:bg-primary-blue/20 transition-colors" className="inline-flex items-center gap-1 rounded-full border border-primary-blue/40 bg-primary-blue/10 px-2 py-1 text-xs text-primary-blue hover:bg-primary-blue/20 transition-colors"
@@ -275,9 +272,9 @@ export default function LeagueDetailPage() {
Discord Discord
</a> </a>
)} )}
{league.socialLinks.youtubeUrl && ( {viewModel.socialLinks.youtubeUrl && (
<a <a
href={league.socialLinks.youtubeUrl} href={viewModel.socialLinks.youtubeUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="inline-flex items-center gap-1 rounded-full border border-red-500/40 bg-red-500/10 px-2 py-1 text-xs text-red-400 hover:bg-red-500/20 transition-colors" className="inline-flex items-center gap-1 rounded-full border border-red-500/40 bg-red-500/10 px-2 py-1 text-xs text-red-400 hover:bg-red-500/20 transition-colors"
@@ -285,9 +282,9 @@ export default function LeagueDetailPage() {
YouTube YouTube
</a> </a>
)} )}
{league.socialLinks.websiteUrl && ( {viewModel.socialLinks.websiteUrl && (
<a <a
href={league.socialLinks.websiteUrl} href={viewModel.socialLinks.websiteUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="inline-flex items-center gap-1 rounded-full border border-charcoal-outline bg-iron-gray/70 px-2 py-1 text-xs text-gray-100 hover:bg-iron-gray transition-colors" className="inline-flex items-center gap-1 rounded-full border border-charcoal-outline bg-iron-gray/70 px-2 py-1 text-xs text-gray-100 hover:bg-iron-gray transition-colors"

View File

@@ -3,9 +3,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { LeagueScoringConfigPresenter } from '@/lib/presenters/LeagueScoringConfigPresenter';
import type { LeagueScoringConfigDTO } from '@core/racing/application/dto/LeagueScoringConfigDTO'; import type { LeagueScoringConfigDTO } from '@core/racing/application/dto/LeagueScoringConfigDTO';
import type { League } from '@core/racing/domain/entities/League'; import { useServices } from '@/lib/services/ServiceProvider';
import type { LeagueWithCapacityDTO } from '@/lib/types/generated/LeagueWithCapacityDTO';
type RulebookSection = 'scoring' | 'conduct' | 'protests' | 'penalties'; type RulebookSection = 'scoring' | 'conduct' | 'protests' | 'penalties';
@@ -13,7 +13,7 @@ export default function LeagueRulebookPage() {
const params = useParams(); const params = useParams();
const leagueId = params.id as string; const leagueId = params.id as string;
const [league, setLeague] = useState<League | null>(null); const [league, setLeague] = useState<LeagueWithCapacityDTO | null>(null);
const [scoringConfig, setScoringConfig] = useState<LeagueScoringConfigDTO | null>(null); const [scoringConfig, setScoringConfig] = useState<LeagueScoringConfigDTO | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeSection, setActiveSection] = useState<RulebookSection>('scoring'); const [activeSection, setActiveSection] = useState<RulebookSection>('scoring');
@@ -21,21 +21,15 @@ export default function LeagueRulebookPage() {
useEffect(() => { useEffect(() => {
async function loadData() { async function loadData() {
try { try {
const leagueRepo = getLeagueRepository(); const { leagueService } = useServices();
const scoringUseCase = getGetLeagueScoringConfigUseCase(); const viewModel = await leagueService.getLeagueDetailPageData(leagueId);
if (!viewModel) {
const leagueData = await leagueRepo.findById(leagueId);
if (!leagueData) {
setLoading(false); setLoading(false);
return; return;
} }
setLeague(leagueData); setLeague(viewModel.league);
setScoringConfig(viewModel.scoringConfig);
const scoringPresenter = new LeagueScoringConfigPresenter();
await scoringUseCase.execute({ leagueId }, scoringPresenter);
const scoringViewModel = scoringPresenter.getViewModel();
setScoringConfig(scoringViewModel as unknown as LeagueScoringConfigDTO);
} catch (err) { } catch (err) {
console.error('Failed to load scoring config:', err); console.error('Failed to load scoring config:', err);
} finally { } finally {

View File

@@ -6,7 +6,7 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { ServiceFactory } from '@/lib/services/ServiceFactory'; import { useServices } from '@/lib/services/ServiceProvider';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { DriverDTO } from '@/lib/types/DriverDTO'; import type { DriverDTO } from '@/lib/types/DriverDTO';
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel'; import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
@@ -18,6 +18,7 @@ export default function LeagueSettingsPage() {
const params = useParams(); const params = useParams();
const leagueId = params.id as string; const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();
const { leagueMembershipService, leagueSettingsService } = useServices();
const [settings, setSettings] = useState<LeagueSettingsViewModel | null>(null); const [settings, setSettings] = useState<LeagueSettingsViewModel | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -27,10 +28,6 @@ export default function LeagueSettingsPage() {
const [transferring, setTransferring] = useState(false); const [transferring, setTransferring] = useState(false);
const router = useRouter(); const router = useRouter();
const serviceFactory = useMemo(() => new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || ''), []);
const leagueMembershipService = useMemo(() => serviceFactory.createLeagueMembershipService(), [serviceFactory]);
const leagueSettingsService = useMemo(() => serviceFactory.createLeagueSettingsService(), [serviceFactory]);
useEffect(() => { useEffect(() => {
async function checkAdmin() { async function checkAdmin() {
const memberships = await leagueMembershipService.fetchLeagueMemberships(leagueId); const memberships = await leagueMembershipService.fetchLeagueMemberships(leagueId);

View File

@@ -5,7 +5,7 @@ import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import type { DriverDto, LeagueMembership } from '@/lib/dtos'; import type { DriverDto, LeagueMembership } from '@/lib/dtos';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { getLeagueStandings } from '@/lib/services/leagues/LeagueService'; import { useServices } from '@/lib/services/ServiceProvider';
import type { LeagueStandingsViewModel } from '@/lib/view-models'; import type { LeagueStandingsViewModel } from '@/lib/view-models';
import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel'; import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
@@ -15,6 +15,7 @@ export default function LeagueStandingsPage() {
const params = useParams(); const params = useParams();
const leagueId = params.id as string; const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();
const { leagueService } = useServices();
const [standings, setStandings] = useState<StandingEntryViewModel[]>([]); const [standings, setStandings] = useState<StandingEntryViewModel[]>([]);
const [drivers, setDrivers] = useState<DriverDto[]>([]); const [drivers, setDrivers] = useState<DriverDto[]>([]);
@@ -26,7 +27,7 @@ export default function LeagueStandingsPage() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
try { try {
const vm = await getLeagueStandings(leagueId, currentDriverId); const vm = await leagueService.getLeagueStandings(leagueId, currentDriverId);
setViewModel(vm); setViewModel(vm);
setStandings(vm.standings); setStandings(vm.standings);
setDrivers(vm.drivers); setDrivers(vm.drivers);
@@ -40,7 +41,7 @@ export default function LeagueStandingsPage() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [leagueId, currentDriverId]); }, [leagueId, currentDriverId, leagueService]);
useEffect(() => { useEffect(() => {
loadData(); loadData();

View File

@@ -4,7 +4,7 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { ServiceFactory } from '@/lib/services/ServiceFactory'; import { useServices } from '@/lib/services/ServiceProvider';
import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel'; import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
import { ProtestDecisionCommandModel, type PenaltyType } from '@/lib/command-models/protests/ProtestDecisionCommandModel'; import { ProtestDecisionCommandModel, type PenaltyType } from '@/lib/command-models/protests/ProtestDecisionCommandModel';
import type { DriverSummaryDTO } from '@/lib/types/generated/LeagueAdminProtestsDTO'; import type { DriverSummaryDTO } from '@/lib/types/generated/LeagueAdminProtestsDTO';
@@ -114,6 +114,7 @@ export default function ProtestReviewPage() {
const leagueId = params.id as string; const leagueId = params.id as string;
const protestId = params.protestId as string; const protestId = params.protestId as string;
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();
const { protestService } = useServices();
const [protest, setProtest] = useState<ProtestViewModel | null>(null); const [protest, setProtest] = useState<ProtestViewModel | null>(null);
const [race, setRace] = useState<RaceDTO | null>(null); const [race, setRace] = useState<RaceDTO | null>(null);
@@ -146,9 +147,6 @@ export default function ProtestReviewPage() {
async function loadProtest() { async function loadProtest() {
setLoading(true); setLoading(true);
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_URL || '');
const protestService = serviceFactory.createProtestService();
const protestData = await protestService.getProtestById(leagueId, protestId); const protestData = await protestService.getProtestById(leagueId, protestId);
if (!protestData) { if (!protestData) {
throw new Error('Protest not found'); throw new Error('Protest not found');
@@ -212,9 +210,6 @@ export default function ProtestReviewPage() {
setSubmitting(true); setSubmitting(true);
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_URL || '');
const protestService = serviceFactory.createProtestService();
if (decision === 'uphold') { if (decision === 'uphold') {
const commandModel = new ProtestDecisionCommandModel({ const commandModel = new ProtestDecisionCommandModel({
decision, decision,
@@ -264,9 +259,6 @@ export default function ProtestReviewPage() {
if (!protest) return; if (!protest) return;
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_URL || '');
const protestService = serviceFactory.createProtestService();
// Request defense // Request defense
await protestService.requestDefense({ await protestService.requestDefense({
protestId: protest.id, protestId: protest.id,

View File

@@ -30,8 +30,8 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import type { LeagueSummaryViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter'; import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import { AllLeaguesWithCapacityAndScoringPresenter } from '@/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter'; import { useServices } from '@/lib/services/ServiceProvider';
// ============================================================================ // ============================================================================
@@ -390,11 +390,9 @@ export default function LeaguesPage() {
const loadLeagues = async () => { const loadLeagues = async () => {
try { try {
const useCase = getGetAllLeaguesWithCapacityAndScoringUseCase(); const { leagueService } = useServices();
const presenter = new AllLeaguesWithCapacityAndScoringPresenter(); const leagues = await leagueService.getAllLeagues();
await useCase.execute(undefined as void, presenter); setRealLeagues(leagues);
const viewModel = presenter.getViewModel();
setRealLeagues(viewModel?.leagues ?? []);
} catch (error) { } catch (error) {
console.error('Failed to load leagues:', error); console.error('Failed to load leagues:', error);
} finally { } finally {

View File

@@ -12,7 +12,7 @@ import type {
ProfileOverviewAchievementViewModel, ProfileOverviewAchievementViewModel,
ProfileOverviewSocialHandleViewModel, ProfileOverviewSocialHandleViewModel,
ProfileOverviewViewModel ProfileOverviewViewModel
} from '@core/racing/application/presenters/IProfileOverviewPresenter'; } from '@/lib/view-models/ProfileOverviewViewModel';
import { import {
Activity, Activity,
Award, Award,

View File

@@ -9,7 +9,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { PendingSponsorshipRequestsPresenter } from '@/lib/presenters/PendingSponsorshipRequestsPresenter'; import { useServices } from '@/lib/services/ServiceProvider';
import { AlertTriangle, Building, ChevronRight, Handshake, Trophy, User, Users } from 'lucide-react'; import { AlertTriangle, Building, ChevronRight, Handshake, Trophy, User, Users } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
@@ -33,25 +33,20 @@ export default function SponsorshipRequestsPage() {
setError(null); setError(null);
try { try {
const { sponsorshipService } = useServices();
const driverRepo = getDriverRepository(); const driverRepo = getDriverRepository();
const leagueRepo = getLeagueRepository(); const leagueRepo = getLeagueRepository();
const teamRepo = getTeamRepository(); const teamRepo = getTeamRepository();
const leagueMembershipRepo = getLeagueMembershipRepository(); const leagueMembershipRepo = getLeagueMembershipRepository();
const teamMembershipRepo = getTeamMembershipRepository(); const teamMembershipRepo = getTeamMembershipRepository();
const useCase = getGetPendingSponsorshipRequestsUseCase();
const allSections: EntitySection[] = []; const allSections: EntitySection[] = [];
// 1. Driver's own sponsorship requests // 1. Driver's own sponsorship requests
const driverPresenter = new PendingSponsorshipRequestsPresenter(); const driverResult = await sponsorshipService.getPendingSponsorshipRequests({
await useCase.execute( entityType: 'driver',
{ entityId: currentDriverId,
entityType: 'driver', });
entityId: currentDriverId,
},
driverPresenter,
);
const driverResult = driverPresenter.getViewModel();
if (driverResult && driverResult.requests.length > 0) { if (driverResult && driverResult.requests.length > 0) {
const driver = await driverRepo.findById(currentDriverId); const driver = await driverRepo.findById(currentDriverId);
@@ -71,15 +66,10 @@ export default function SponsorshipRequestsPage() {
// Load sponsorship requests for this league's active season // Load sponsorship requests for this league's active season
try { try {
// For simplicity, we'll query by season entityType - in production you'd get the active season ID // For simplicity, we'll query by season entityType - in production you'd get the active season ID
const leaguePresenter = new PendingSponsorshipRequestsPresenter(); const leagueResult = await sponsorshipService.getPendingSponsorshipRequests({
await useCase.execute( entityType: 'season',
{ entityId: league.id, // Using league ID as a proxy for now
entityType: 'season', });
entityId: league.id, // Using league ID as a proxy for now
},
leaguePresenter,
);
const leagueResult = leaguePresenter.getViewModel();
if (leagueResult && leagueResult.requests.length > 0) { if (leagueResult && leagueResult.requests.length > 0) {
allSections.push({ allSections.push({
@@ -100,15 +90,10 @@ export default function SponsorshipRequestsPage() {
for (const team of allTeams) { for (const team of allTeams) {
const membership = await teamMembershipRepo.getMembership(team.id, currentDriverId); const membership = await teamMembershipRepo.getMembership(team.id, currentDriverId);
if (membership && (membership.role === 'owner' || membership.role === 'manager')) { if (membership && (membership.role === 'owner' || membership.role === 'manager')) {
const teamPresenter = new PendingSponsorshipRequestsPresenter(); const teamResult = await sponsorshipService.getPendingSponsorshipRequests({
await useCase.execute( entityType: 'team',
{ entityId: team.id,
entityType: 'team', });
entityId: team.id,
},
teamPresenter,
);
const teamResult = teamPresenter.getViewModel();
if (teamResult && teamResult.requests.length > 0) { if (teamResult && teamResult.requests.length > 0) {
allSections.push({ allSections.push({

View File

@@ -8,8 +8,7 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { ServiceFactory } from '@/lib/services/ServiceFactory'; import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
import { LeagueMembershipUtility } from '@/lib/utilities/LeagueMembershipUtility'; import { LeagueMembershipUtility } from '@/lib/utilities/LeagueMembershipUtility';
import type { RaceDetailViewModel } from '@/lib/view-models/RaceDetailViewModel'; import type { RaceDetailViewModel } from '@/lib/view-models/RaceDetailViewModel';
import { import {
@@ -38,6 +37,7 @@ export default function RaceDetailPage() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const raceId = params.id as string; const raceId = params.id as string;
const { raceService, leagueMembershipService } = useServices();
const [viewModel, setViewModel] = useState<RaceDetailViewModel | null>(null); const [viewModel, setViewModel] = useState<RaceDetailViewModel | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -57,15 +57,13 @@ export default function RaceDetailPage() {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const raceService = serviceFactory.createRaceService();
const vm = await raceService.getRaceDetail(raceId, currentDriverId); const vm = await raceService.getRaceDetail(raceId, currentDriverId);
setViewModel(vm); setViewModel(vm);
// Fetch league membership for admin controls // Fetch league membership for admin controls
if (vm.league) { if (vm.league) {
await LeagueMembershipService.fetchLeagueMemberships(vm.league.id); await leagueMembershipService.fetchLeagueMemberships(vm.league.id);
const leagueMembership = LeagueMembershipService.getMembership(vm.league.id, currentDriverId); const leagueMembership = leagueMembershipService.getMembership(vm.league.id, currentDriverId);
setMembership(leagueMembership); setMembership(leagueMembership);
} }
@@ -123,8 +121,6 @@ export default function RaceDetailPage() {
setCancelling(true); setCancelling(true);
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const raceService = serviceFactory.createRaceService();
await raceService.cancelRace(race.id); await raceService.cancelRace(race.id);
await loadRaceData(); await loadRaceData();
} catch (err) { } catch (err) {
@@ -147,8 +143,6 @@ export default function RaceDetailPage() {
setRegistering(true); setRegistering(true);
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const raceService = serviceFactory.createRaceService();
await raceService.registerForRace(race.id, league.id, currentDriverId); await raceService.registerForRace(race.id, league.id, currentDriverId);
await loadRaceData(); await loadRaceData();
} catch (err) { } catch (err) {
@@ -171,8 +165,6 @@ export default function RaceDetailPage() {
setRegistering(true); setRegistering(true);
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const raceService = serviceFactory.createRaceService();
await raceService.withdrawFromRace(race.id, currentDriverId); await raceService.withdrawFromRace(race.id, currentDriverId);
await loadRaceData(); await loadRaceData();
} catch (err) { } catch (err) {
@@ -973,8 +965,6 @@ export default function RaceDetailPage() {
raceName={race.track} raceName={race.track}
onConfirm={async () => { onConfirm={async () => {
try { try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const raceService = serviceFactory.createRaceService();
await raceService.completeRace(race.id); await raceService.completeRace(race.id);
await loadRaceData(); await loadRaceData();
setShowEndRaceModal(false); setShowEndRaceModal(false);

View File

@@ -7,7 +7,7 @@ import ResultsTable from '@/components/races/ResultsTable';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { raceResultsService } from '@/lib/services/races/RaceResultsService'; import { useServices } from '@/lib/services/ServiceProvider';
import type { RaceResultsDetailViewModel } from '@/lib/view-models'; import type { RaceResultsDetailViewModel } from '@/lib/view-models';
import { ArrowLeft, Calendar, Trophy, Users, Zap } from 'lucide-react'; import { ArrowLeft, Calendar, Trophy, Users, Zap } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -18,6 +18,7 @@ export default function RaceResultsPage() {
const params = useParams(); const params = useParams();
const raceId = params.id as string; const raceId = params.id as string;
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();
const { raceResultsService } = useServices();
const [raceData, setRaceData] = useState<RaceResultsDetailViewModel | null>(null); const [raceData, setRaceData] = useState<RaceResultsDetailViewModel | null>(null);
const [raceSOF, setRaceSOF] = useState<number | null>(null); const [raceSOF, setRaceSOF] = useState<number | null>(null);

View File

@@ -12,7 +12,7 @@ import JoinTeamButton from '@/components/teams/JoinTeamButton';
import TeamAdmin from '@/components/teams/TeamAdmin'; import TeamAdmin from '@/components/teams/TeamAdmin';
import TeamRoster from '@/components/teams/TeamRoster'; import TeamRoster from '@/components/teams/TeamRoster';
import TeamStandings from '@/components/teams/TeamStandings'; import TeamStandings from '@/components/teams/TeamStandings';
import { ServiceFactory } from '@/lib/services/ServiceFactory'; import { useServices } from '@/lib/services/ServiceProvider';
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel'; import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel'; import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
@@ -29,21 +29,17 @@ interface TeamMembership {
type Tab = 'overview' | 'roster' | 'standings' | 'admin'; type Tab = 'overview' | 'roster' | 'standings' | 'admin';
export default function TeamDetailPage() { export default function TeamDetailPage() {
const params = useParams(); const params = useParams();
const teamId = params.id as string; const teamId = params.id as string;
const { teamService, mediaService } = useServices();
const [team, setTeam] = useState<TeamDetailsViewModel | null>(null); const [team, setTeam] = useState<TeamDetailsViewModel | null>(null);
const [memberships, setMemberships] = useState<TeamMemberViewModel[]>([]); const [memberships, setMemberships] = useState<TeamMemberViewModel[]>([]);
const [activeTab, setActiveTab] = useState<Tab>('overview'); const [activeTab, setActiveTab] = useState<Tab>('overview');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();
const isSponsorMode = useSponsorMode(); const isSponsorMode = useSponsorMode();
// Initialize services
const serviceFactory = useMemo(() => new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || ''), []);
const teamService = useMemo(() => serviceFactory.createTeamService(), [serviceFactory]);
const mediaService = useMemo(() => serviceFactory.createMediaService(), [serviceFactory]);
const loadTeamData = useCallback(async () => { const loadTeamData = useCallback(async () => {
setLoading(true); setLoading(true);

View File

@@ -22,19 +22,18 @@ import {
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter'; import { useServices } from '@/lib/services/ServiceProvider';
import type { import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
TeamLeaderboardItemViewModel,
SkillLevel,
} from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
// ============================================================================ // ============================================================================
// TYPES // TYPES
// ============================================================================ // ============================================================================
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races'; type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
type TeamDisplayData = TeamLeaderboardItemViewModel; type TeamDisplayData = TeamSummaryViewModel;
const getSafeRating = (team: TeamDisplayData): number => { const getSafeRating = (team: TeamDisplayData): number => {
const value = typeof team.rating === 'number' ? team.rating : 0; const value = typeof team.rating === 'number' ? team.rating : 0;
@@ -266,15 +265,9 @@ export default function TeamLeaderboardPage() {
useEffect(() => { useEffect(() => {
const loadTeams = async () => { const loadTeams = async () => {
try { try {
const useCase = getGetTeamsLeaderboardUseCase(); const { teamService } = useServices();
const presenter = new TeamsLeaderboardPresenter(); const teams = await teamService.getAllTeams();
setTeams(teams);
await useCase.execute(undefined as void, presenter);
const viewModel = presenter.getViewModel();
if (viewModel) {
setTeams(viewModel.teams);
}
} catch (error) { } catch (error) {
console.error('Failed to load teams:', error); console.error('Failed to load teams:', error);
} finally { } finally {

View File

@@ -28,8 +28,8 @@ import Card from '@/components/ui/Card';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import CreateTeamForm from '@/components/teams/CreateTeamForm'; import CreateTeamForm from '@/components/teams/CreateTeamForm';
import { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter'; import { useServices } from '@/lib/services/ServiceProvider';
import type { TeamLeaderboardItemViewModel, SkillLevel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter'; import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
// ============================================================================ // ============================================================================
// TYPES // TYPES
@@ -448,17 +448,12 @@ export default function TeamsPage() {
const loadTeams = async () => { const loadTeams = async () => {
try { try {
const useCase = getGetTeamsLeaderboardUseCase(); const { teamService } = useServices();
const presenter = new TeamsLeaderboardPresenter(); const teams = await teamService.getAllTeams();
setRealTeams(teams);
await useCase.execute(undefined as void, presenter); // TODO: set groups and top teams from service or compute locally
setGroupsBySkillLevel({});
const viewModel = presenter.getViewModel(); setTopTeams([]);
if (viewModel) {
setRealTeams(viewModel.teams);
setGroupsBySkillLevel(viewModel.groupsBySkillLevel);
setTopTeams(viewModel.topTeams);
}
} catch (error) { } catch (error) {
console.error('Failed to load teams:', error); console.error('Failed to load teams:', error);
} finally { } finally {

View File

@@ -0,0 +1,101 @@
'use client';
import React, { createContext, useContext, useMemo, ReactNode } from 'react';
import { ServiceFactory } from './ServiceFactory';
// Import all service types
import { RaceService } from './races/RaceService';
import { RaceResultsService } from './races/RaceResultsService';
import { DriverService } from './drivers/DriverService';
import { DriverRegistrationService } from './drivers/DriverRegistrationService';
import { TeamService } from './teams/TeamService';
import { TeamJoinService } from './teams/TeamJoinService';
import { LeagueService } from './leagues/LeagueService';
import { LeagueMembershipService } from './leagues/LeagueMembershipService';
import { LeagueSettingsService } from './leagues/LeagueSettingsService';
import { SponsorService } from './sponsors/SponsorService';
import { SponsorshipService } from './sponsors/SponsorshipService';
import { PaymentService } from './payments/PaymentService';
import { AnalyticsService } from './analytics/AnalyticsService';
import { DashboardService } from './dashboard/DashboardService';
import { MediaService } from './media/MediaService';
import { AvatarService } from './media/AvatarService';
import { WalletService } from './payments/WalletService';
import { MembershipFeeService } from './payments/MembershipFeeService';
import { AuthService } from './auth/AuthService';
import { SessionService } from './auth/SessionService';
import { ProtestService } from './protests/ProtestService';
export interface Services {
raceService: RaceService;
raceResultsService: RaceResultsService;
driverService: DriverService;
driverRegistrationService: DriverRegistrationService;
teamService: TeamService;
teamJoinService: TeamJoinService;
leagueService: LeagueService;
leagueMembershipService: LeagueMembershipService;
leagueSettingsService: LeagueSettingsService;
sponsorService: SponsorService;
sponsorshipService: SponsorshipService;
paymentService: PaymentService;
analyticsService: AnalyticsService;
dashboardService: DashboardService;
mediaService: MediaService;
avatarService: AvatarService;
walletService: WalletService;
membershipFeeService: MembershipFeeService;
authService: AuthService;
sessionService: SessionService;
protestService: ProtestService;
}
const ServicesContext = createContext<Services | null>(null);
interface ServiceProviderProps {
children: ReactNode;
}
export function ServiceProvider({ children }: ServiceProviderProps) {
const services = useMemo(() => {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
return {
raceService: serviceFactory.createRaceService(),
raceResultsService: serviceFactory.createRaceResultsService(),
driverService: serviceFactory.createDriverService(),
driverRegistrationService: serviceFactory.createDriverRegistrationService(),
teamService: serviceFactory.createTeamService(),
teamJoinService: serviceFactory.createTeamJoinService(),
leagueService: serviceFactory.createLeagueService(),
leagueMembershipService: serviceFactory.createLeagueMembershipService(),
leagueSettingsService: serviceFactory.createLeagueSettingsService(),
sponsorService: serviceFactory.createSponsorService(),
sponsorshipService: serviceFactory.createSponsorshipService(),
paymentService: serviceFactory.createPaymentService(),
analyticsService: serviceFactory.createAnalyticsService(),
dashboardService: serviceFactory.createDashboardService(),
mediaService: serviceFactory.createMediaService(),
avatarService: serviceFactory.createAvatarService(),
walletService: serviceFactory.createWalletService(),
membershipFeeService: serviceFactory.createMembershipFeeService(),
authService: serviceFactory.createAuthService(),
sessionService: serviceFactory.createSessionService(),
protestService: serviceFactory.createProtestService(),
};
}, []);
return (
<ServicesContext.Provider value={services}>
{children}
</ServicesContext.Provider>
);
}
export function useServices(): Services {
const services = useContext(ServicesContext);
if (!services) {
throw new Error('useServices must be used within a ServiceProvider');
}
return services;
}

View File

@@ -7,6 +7,10 @@ export class LeagueMembershipService {
// In-memory cache for memberships (populated via API calls) // In-memory cache for memberships (populated via API calls)
private static leagueMemberships = new Map<string, LeagueMembership[]>(); private static leagueMemberships = new Map<string, LeagueMembership[]>();
constructor() {
// Constructor for dependency injection, but this service uses static methods
}
/** /**
* Get a specific membership from cache. * Get a specific membership from cache.
*/ */
@@ -65,4 +69,41 @@ export class LeagueMembershipService {
static getCachedMembershipsIterator(): IterableIterator<[string, LeagueMembership[]]> { static getCachedMembershipsIterator(): IterableIterator<[string, LeagueMembership[]]> {
return this.leagueMemberships.entries(); return this.leagueMemberships.entries();
} }
// Instance methods that delegate to static methods for consistency with service pattern
/**
* Get a specific membership from cache.
*/
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
return LeagueMembershipService.getMembership(leagueId, driverId);
}
/**
* Get all members of a league from cache.
*/
getLeagueMembers(leagueId: string): LeagueMembership[] {
return LeagueMembershipService.getLeagueMembers(leagueId);
}
/**
* Fetch and cache memberships for a league via API.
*/
async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
return LeagueMembershipService.fetchLeagueMemberships(leagueId);
}
/**
* Set memberships in cache (for use after API calls).
*/
setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
LeagueMembershipService.setLeagueMemberships(leagueId, memberships);
}
/**
* Clear cached memberships for a league.
*/
clearLeagueMemberships(leagueId: string): void {
LeagueMembershipService.clearLeagueMemberships(leagueId);
}
} }

View File

@@ -26,8 +26,8 @@ export class SponsorshipService {
} }
/** /**
* Get sponsor sponsorships with view model transformation * Get sponsor sponsorships with view model transformation
*/ */
async getSponsorSponsorships(sponsorId: string): Promise<SponsorSponsorshipsViewModel | null> { async getSponsorSponsorships(sponsorId: string): Promise<SponsorSponsorshipsViewModel | null> {
const dto = await this.apiClient.getSponsorships(sponsorId); const dto = await this.apiClient.getSponsorships(sponsorId);
if (!dto) { if (!dto) {
@@ -35,4 +35,13 @@ export class SponsorshipService {
} }
return new SponsorSponsorshipsViewModel(dto); return new SponsorSponsorshipsViewModel(dto);
} }
/**
* Get pending sponsorship requests for an entity
*/
async getPendingSponsorshipRequests(params: { entityType: string; entityId: string }): Promise<{ requests: any[] }> {
// TODO: Implement API call
// For now, return empty
return { requests: [] };
}
} }

View File

@@ -11,11 +11,12 @@ export class DriverLeaderboardItemViewModel {
podiums: number; podiums: number;
isActive: boolean; isActive: boolean;
rank: number; rank: number;
avatarUrl: string;
position: number; position: number;
private previousRating?: number; private previousRating?: number;
constructor(dto: DriverLeaderboardItemDTO, position: number, previousRating?: number) { constructor(dto: DriverLeaderboardItemDTO & { avatarUrl: string }, position: number, previousRating?: number) {
this.id = dto.id; this.id = dto.id;
this.name = dto.name; this.name = dto.name;
this.rating = dto.rating; this.rating = dto.rating;
@@ -26,6 +27,7 @@ export class DriverLeaderboardItemViewModel {
this.podiums = dto.podiums; this.podiums = dto.podiums;
this.isActive = dto.isActive; this.isActive = dto.isActive;
this.rank = dto.rank; this.rank = dto.rank;
this.avatarUrl = dto.avatarUrl;
this.position = position; this.position = position;
this.previousRating = previousRating; this.previousRating = previousRating;
} }

View File

@@ -1,67 +1,23 @@
import { LeagueSummaryDTO } from '../types/generated/LeagueSummaryDTO'; export interface LeagueSummaryViewModel {
export class LeagueSummaryViewModel {
id: string; id: string;
name: string; name: string;
description: string;
constructor(dto: LeagueSummaryDTO) { ownerId: string;
this.id = dto.id; createdAt: string;
this.name = dto.name; maxDrivers: number;
} usedDriverSlots: number;
maxTeams?: number;
// Note: The generated DTO only has id and name usedTeamSlots?: number;
// These fields will need to be added when the OpenAPI spec is updated structureSummary: string;
description?: string; scoringPatternSummary?: string;
logoUrl?: string; timingSummary: string;
coverImage?: string; scoring?: {
memberCount: number = 0; gameId: string;
maxMembers: number = 0; gameName: string;
isPublic: boolean = false; primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
ownerId: string = ''; scoringPresetId: string;
ownerName?: string; scoringPresetName: string;
scoringType?: string; dropPolicySummary: string;
status?: string; scoringPatternSummary: string;
};
/** UI-specific: Formatted capacity display */
get formattedCapacity(): string {
return `${this.memberCount}/${this.maxMembers}`;
}
/** UI-specific: Capacity bar percentage */
get capacityBarPercent(): number {
return (this.memberCount / this.maxMembers) * 100;
}
/** UI-specific: Label for join button */
get joinButtonLabel(): string {
if (this.isFull) return 'Full';
return this.isJoinable ? 'Join League' : 'Request to Join';
}
/** UI-specific: Whether the league is full */
get isFull(): boolean {
return this.memberCount >= this.maxMembers;
}
/** UI-specific: Whether the league is joinable */
get isJoinable(): boolean {
return this.isPublic && !this.isFull;
}
/** UI-specific: Color for member progress */
get memberProgressColor(): string {
const percent = this.capacityBarPercent;
if (percent < 50) return 'green';
if (percent < 80) return 'yellow';
return 'red';
}
/** UI-specific: Badge variant for status */
get statusBadgeVariant(): string {
switch (this.status) {
case 'active': return 'success';
case 'inactive': return 'secondary';
default: return 'default';
}
}
} }

View File

@@ -0,0 +1,100 @@
export interface ProfileOverviewDriverSummaryViewModel {
id: string;
name: string;
country: string;
avatarUrl: string;
iracingId: string | null;
joinedAt: string;
rating: number | null;
globalRank: number | null;
consistency: number | null;
bio: string | null;
totalDrivers: number | null;
}
export interface ProfileOverviewStatsViewModel {
totalRaces: number;
wins: number;
podiums: number;
dnfs: number;
avgFinish: number | null;
bestFinish: number | null;
worstFinish: number | null;
finishRate: number | null;
winRate: number | null;
podiumRate: number | null;
percentile: number | null;
rating: number | null;
consistency: number | null;
overallRank: number | null;
}
export interface ProfileOverviewFinishDistributionViewModel {
totalRaces: number;
wins: number;
podiums: number;
topTen: number;
dnfs: number;
other: number;
}
export interface ProfileOverviewTeamMembershipViewModel {
teamId: string;
teamName: string;
teamTag: string | null;
role: string;
joinedAt: string;
isCurrent: boolean;
}
export interface ProfileOverviewSocialFriendSummaryViewModel {
id: string;
name: string;
country: string;
avatarUrl: string;
}
export interface ProfileOverviewSocialSummaryViewModel {
friendsCount: number;
friends: ProfileOverviewSocialFriendSummaryViewModel[];
}
export type ProfileOverviewSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
export type ProfileOverviewAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
export interface ProfileOverviewAchievementViewModel {
id: string;
title: string;
description: string;
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
rarity: ProfileOverviewAchievementRarity;
earnedAt: string;
}
export interface ProfileOverviewSocialHandleViewModel {
platform: ProfileOverviewSocialPlatform;
handle: string;
url: string;
}
export interface ProfileOverviewExtendedProfileViewModel {
socialHandles: ProfileOverviewSocialHandleViewModel[];
achievements: ProfileOverviewAchievementViewModel[];
racingStyle: string;
favoriteTrack: string;
favoriteCar: string;
timezone: string;
availableHours: string;
lookingForTeam: boolean;
openToRequests: boolean;
}
export interface ProfileOverviewViewModel {
currentDriver: ProfileOverviewDriverSummaryViewModel | null;
stats: ProfileOverviewStatsViewModel | null;
finishDistribution: ProfileOverviewFinishDistributionViewModel | null;
teamMemberships: ProfileOverviewTeamMembershipViewModel[];
socialSummary: ProfileOverviewSocialSummaryViewModel;
extendedProfile: ProfileOverviewExtendedProfileViewModel | null;
}

View File

@@ -13,15 +13,31 @@ export class TeamSummaryViewModel {
logoUrl?: string; logoUrl?: string;
memberCount: number; memberCount: number;
rating: number; rating: number;
description?: string;
totalWins: number = 0;
totalRaces: number = 0;
performanceLevel: string = '';
isRecruiting: boolean = false;
specialization?: string;
region?: string;
languages: string[] = [];
private maxMembers = 10; // Assuming max members private maxMembers = 10; // Assuming max members
constructor(dto: TeamSummaryDTO) { constructor(dto: TeamSummaryDTO & { description?: string; totalWins?: number; totalRaces?: number; performanceLevel?: string; isRecruiting?: boolean; specialization?: string; region?: string; languages?: string[] }) {
this.id = dto.id; this.id = dto.id;
this.name = dto.name; this.name = dto.name;
if (dto.logoUrl !== undefined) this.logoUrl = dto.logoUrl; if (dto.logoUrl !== undefined) this.logoUrl = dto.logoUrl;
this.memberCount = dto.memberCount; this.memberCount = dto.memberCount;
this.rating = dto.rating; this.rating = dto.rating;
this.description = dto.description;
this.totalWins = dto.totalWins ?? 0;
this.totalRaces = dto.totalRaces ?? 0;
this.performanceLevel = dto.performanceLevel ?? '';
this.isRecruiting = dto.isRecruiting ?? false;
this.specialization = dto.specialization;
this.region = dto.region;
this.languages = dto.languages ?? [];
} }
/** UI-specific: Whether team is full */ /** UI-specific: Whether team is full */