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

View File

@@ -36,7 +36,7 @@ import {
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
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';
// ============================================================================
@@ -314,6 +314,7 @@ export default function DriverDetailPage() {
const router = useRouter();
const params = useParams();
const driverId = params.id as string;
const { driverService, teamService } = useServices();
const [driverProfile, setDriverProfile] = useState<DriverProfileViewModel | null>(null);
const [loading, setLoading] = useState(true);
@@ -354,11 +355,6 @@ export default function DriverDetailPage() {
const loadDriver = async () => {
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
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 Card from '@/components/ui/Card';
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 Image from 'next/image';
@@ -373,6 +373,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
export default function DriversPage() {
const router = useRouter();
const { driverService } = useServices();
const [drivers, setDrivers] = useState<DriverLeaderboardItemViewModel[]>([]);
const [viewModel, setViewModel] = useState<DriverLeaderboardViewModel | null>(null);
const [loading, setLoading] = useState(true);
@@ -383,7 +384,7 @@ export default function DriversPage() {
useEffect(() => {
const load = async () => {
const vm = await getDriverLeaderboard();
const vm = await driverService.getDriverLeaderboard();
setViewModel(vm);
setDrivers(vm.drivers);
setTotalRaces(vm.totalRaces);
@@ -393,7 +394,7 @@ export default function DriversPage() {
};
void load();
}, []);
}, [driverService]);
const handleDriverClick = (driverId: string) => {
router.push(`/drivers/${driverId}`);

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,9 +13,8 @@ import SponsorInsightsCard, {
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
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 { Calendar, ExternalLink, Star, Trophy, Users } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation';
@@ -26,6 +25,7 @@ export default function LeagueDetailPage() {
const params = useParams();
const leagueId = params.id as string;
const isSponsor = useSponsorMode();
const { leagueService, leagueMembershipService } = useServices();
const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null);
const [loading, setLoading] = useState(true);
@@ -33,8 +33,8 @@ export default function LeagueDetailPage() {
const [endRaceModalRaceId, setEndRaceModalRaceId] = useState<string | null>(null);
const currentDriverId = useEffectiveDriverId();
const membership = LeagueMembershipService.getMembership(leagueId, currentDriverId);
const leagueMemberships = LeagueMembershipService.getLeagueMembers(leagueId);
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
const leagueMemberships = leagueMembershipService.getLeagueMembers(leagueId);
// Build metrics for SponsorInsightsCard
const leagueMetrics: SponsorMetric[] = useMemo(() => {
@@ -49,9 +49,6 @@ export default function LeagueDetailPage() {
const loadLeagueData = async () => {
try {
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_URL || '');
const leagueService = serviceFactory.createLeagueService();
const viewModelData = await leagueService.getLeagueDetailPageData(leagueId);
if (!viewModelData) {
@@ -265,9 +262,9 @@ export default function LeagueDetailPage() {
{viewModel.socialLinks && (
<div className="mt-4 pt-4 border-t border-charcoal-outline">
<div className="flex flex-wrap gap-2">
{league.socialLinks.discordUrl && (
{viewModel.socialLinks.discordUrl && (
<a
href={league.socialLinks.discordUrl}
href={viewModel.socialLinks.discordUrl}
target="_blank"
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"
@@ -275,9 +272,9 @@ export default function LeagueDetailPage() {
Discord
</a>
)}
{league.socialLinks.youtubeUrl && (
{viewModel.socialLinks.youtubeUrl && (
<a
href={league.socialLinks.youtubeUrl}
href={viewModel.socialLinks.youtubeUrl}
target="_blank"
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"
@@ -285,9 +282,9 @@ export default function LeagueDetailPage() {
YouTube
</a>
)}
{league.socialLinks.websiteUrl && (
{viewModel.socialLinks.websiteUrl && (
<a
href={league.socialLinks.websiteUrl}
href={viewModel.socialLinks.websiteUrl}
target="_blank"
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"

View File

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

View File

@@ -6,7 +6,7 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
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 { DriverDTO } from '@/lib/types/DriverDTO';
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
@@ -18,6 +18,7 @@ export default function LeagueSettingsPage() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const { leagueMembershipService, leagueSettingsService } = useServices();
const [settings, setSettings] = useState<LeagueSettingsViewModel | null>(null);
const [loading, setLoading] = useState(true);
@@ -27,10 +28,6 @@ export default function LeagueSettingsPage() {
const [transferring, setTransferring] = useState(false);
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(() => {
async function checkAdmin() {
const memberships = await leagueMembershipService.fetchLeagueMemberships(leagueId);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import ResultsTable from '@/components/races/ResultsTable';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
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 { ArrowLeft, Calendar, Trophy, Users, Zap } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation';
@@ -18,6 +18,7 @@ export default function RaceResultsPage() {
const params = useParams();
const raceId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const { raceResultsService } = useServices();
const [raceData, setRaceData] = useState<RaceResultsDetailViewModel | 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 TeamRoster from '@/components/teams/TeamRoster';
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 { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
@@ -29,21 +29,17 @@ interface TeamMembership {
type Tab = 'overview' | 'roster' | 'standings' | 'admin';
export default function TeamDetailPage() {
const params = useParams();
const teamId = params.id as string;
const params = useParams();
const teamId = params.id as string;
const { teamService, mediaService } = useServices();
const [team, setTeam] = useState<TeamDetailsViewModel | null>(null);
const [memberships, setMemberships] = useState<TeamMemberViewModel[]>([]);
const [activeTab, setActiveTab] = useState<Tab>('overview');
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const currentDriverId = useEffectiveDriverId();
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 [team, setTeam] = useState<TeamDetailsViewModel | null>(null);
const [memberships, setMemberships] = useState<TeamMemberViewModel[]>([]);
const [activeTab, setActiveTab] = useState<Tab>('overview');
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const currentDriverId = useEffectiveDriverId();
const isSponsorMode = useSponsorMode();
const loadTeamData = useCallback(async () => {
setLoading(true);

View File

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

View File

@@ -28,8 +28,8 @@ 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 { TeamsLeaderboardPresenter } from '@/lib/presenters/TeamsLeaderboardPresenter';
import type { TeamLeaderboardItemViewModel, SkillLevel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
import { useServices } from '@/lib/services/ServiceProvider';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
// ============================================================================
// TYPES
@@ -448,17 +448,12 @@ export default function TeamsPage() {
const loadTeams = async () => {
try {
const useCase = getGetTeamsLeaderboardUseCase();
const presenter = new TeamsLeaderboardPresenter();
await useCase.execute(undefined as void, presenter);
const viewModel = presenter.getViewModel();
if (viewModel) {
setRealTeams(viewModel.teams);
setGroupsBySkillLevel(viewModel.groupsBySkillLevel);
setTopTeams(viewModel.topTeams);
}
const { teamService } = useServices();
const teams = await teamService.getAllTeams();
setRealTeams(teams);
// TODO: set groups and top teams from service or compute locally
setGroupsBySkillLevel({});
setTopTeams([]);
} catch (error) {
console.error('Failed to load teams:', error);
} 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)
private static leagueMemberships = new Map<string, LeagueMembership[]>();
constructor() {
// Constructor for dependency injection, but this service uses static methods
}
/**
* Get a specific membership from cache.
*/
@@ -65,4 +69,41 @@ export class LeagueMembershipService {
static getCachedMembershipsIterator(): IterableIterator<[string, LeagueMembership[]]> {
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> {
const dto = await this.apiClient.getSponsorships(sponsorId);
if (!dto) {
@@ -35,4 +35,13 @@ export class SponsorshipService {
}
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;
isActive: boolean;
rank: number;
avatarUrl: string;
position: 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.name = dto.name;
this.rating = dto.rating;
@@ -26,6 +27,7 @@ export class DriverLeaderboardItemViewModel {
this.podiums = dto.podiums;
this.isActive = dto.isActive;
this.rank = dto.rank;
this.avatarUrl = dto.avatarUrl;
this.position = position;
this.previousRating = previousRating;
}

View File

@@ -1,67 +1,23 @@
import { LeagueSummaryDTO } from '../types/generated/LeagueSummaryDTO';
export class LeagueSummaryViewModel {
export interface LeagueSummaryViewModel {
id: string;
name: string;
constructor(dto: LeagueSummaryDTO) {
this.id = dto.id;
this.name = dto.name;
}
// Note: The generated DTO only has id and name
// These fields will need to be added when the OpenAPI spec is updated
description?: string;
logoUrl?: string;
coverImage?: string;
memberCount: number = 0;
maxMembers: number = 0;
isPublic: boolean = false;
ownerId: string = '';
ownerName?: string;
scoringType?: string;
status?: 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';
}
}
description: string;
ownerId: string;
createdAt: string;
maxDrivers: number;
usedDriverSlots: number;
maxTeams?: number;
usedTeamSlots?: number;
structureSummary: string;
scoringPatternSummary?: string;
timingSummary: string;
scoring?: {
gameId: string;
gameName: string;
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
scoringPresetId: string;
scoringPresetName: string;
dropPolicySummary: string;
scoringPatternSummary: string;
};
}

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;
memberCount: 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
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.name = dto.name;
if (dto.logoUrl !== undefined) this.logoUrl = dto.logoUrl;
this.memberCount = dto.memberCount;
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 */