From 9814d9682c95a14f3343471d55572903d7610b31 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 18 Dec 2025 17:02:48 +0100 Subject: [PATCH] refactor page to use services --- apps/website/app/dashboard/page.tsx | 40 ++++--- apps/website/app/drivers/[id]/page.tsx | 8 +- apps/website/app/drivers/page.tsx | 7 +- apps/website/app/layout.tsx | 25 +++-- .../website/app/leaderboards/drivers/page.tsx | 16 ++- apps/website/app/leaderboards/page.tsx | 28 ++--- apps/website/app/leagues/[id]/layout.tsx | 10 +- apps/website/app/leagues/[id]/page.tsx | 23 ++-- .../app/leagues/[id]/rulebook/page.tsx | 22 ++-- .../app/leagues/[id]/settings/page.tsx | 7 +- .../app/leagues/[id]/standings/page.tsx | 7 +- .../stewarding/protests/[protestId]/page.tsx | 12 +-- apps/website/app/leagues/page.tsx | 12 +-- apps/website/app/profile/page.tsx | 2 +- .../app/profile/sponsorship-requests/page.tsx | 45 +++----- apps/website/app/races/[id]/page.tsx | 18 +--- apps/website/app/races/[id]/results/page.tsx | 3 +- apps/website/app/teams/[id]/page.tsx | 26 ++--- apps/website/app/teams/leaderboard/page.tsx | 23 ++-- apps/website/app/teams/page.tsx | 21 ++-- apps/website/lib/services/ServiceProvider.tsx | 101 ++++++++++++++++++ .../leagues/LeagueMembershipService.ts | 41 +++++++ .../services/sponsors/SponsorshipService.ts | 13 ++- .../DriverLeaderboardItemViewModel.ts | 4 +- .../lib/view-models/LeagueSummaryViewModel.ts | 84 ++++----------- .../view-models/ProfileOverviewViewModel.ts | 100 +++++++++++++++++ .../lib/view-models/TeamSummaryViewModel.ts | 18 +++- 27 files changed, 434 insertions(+), 282 deletions(-) create mode 100644 apps/website/lib/services/ServiceProvider.tsx create mode 100644 apps/website/lib/view-models/ProfileOverviewViewModel.ts diff --git a/apps/website/app/dashboard/page.tsx b/apps/website/app/dashboard/page.tsx index 9e81652ef..023eca6ae 100644 --- a/apps/website/app/dashboard/page.tsx +++ b/apps/website/app/dashboard/page.tsx @@ -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(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const { dashboardService } = useServices(); + const [dashboardData, setDashboardData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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 ( diff --git a/apps/website/app/drivers/[id]/page.tsx b/apps/website/app/drivers/[id]/page.tsx index 6fab6f405..5909b1e11 100644 --- a/apps/website/app/drivers/[id]/page.tsx +++ b/apps/website/app/drivers/[id]/page.tsx @@ -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(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); diff --git a/apps/website/app/drivers/page.tsx b/apps/website/app/drivers/page.tsx index 203867735..9d6b9ae00 100644 --- a/apps/website/app/drivers/page.tsx +++ b/apps/website/app/drivers/page.tsx @@ -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([]); const [viewModel, setViewModel] = useState(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}`); diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index f5b467b57..96e7bf90d 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -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({ - - - - -
- {children} -
- - -
-
+ + + + + +
+ {children} +
+ + +
+
+
); diff --git a/apps/website/app/leaderboards/drivers/page.tsx b/apps/website/app/leaderboards/drivers/page.tsx index de7efadfe..561222f73 100644 --- a/apps/website/app/leaderboards/drivers/page.tsx +++ b/apps/website/app/leaderboards/drivers/page.tsx @@ -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); }; diff --git a/apps/website/app/leaderboards/page.tsx b/apps/website/app/leaderboards/page.tsx index 8b1fef1c0..eb79167b9 100644 --- a/apps/website/app/leaderboards/page.tsx +++ b/apps/website/app/leaderboards/page.tsx @@ -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([]); diff --git a/apps/website/app/leagues/[id]/layout.tsx b/apps/website/app/leagues/[id]/layout.tsx index 2874e920e..e6021af7d 100644 --- a/apps/website/app/leagues/[id]/layout.tsx +++ b/apps/website/app/leagues/[id]/layout.tsx @@ -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(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 ( diff --git a/apps/website/app/leagues/[id]/page.tsx b/apps/website/app/leagues/[id]/page.tsx index 5056c14e6..e28100bf2 100644 --- a/apps/website/app/leagues/[id]/page.tsx +++ b/apps/website/app/leagues/[id]/page.tsx @@ -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(null); const [loading, setLoading] = useState(true); @@ -33,8 +33,8 @@ export default function LeagueDetailPage() { const [endRaceModalRaceId, setEndRaceModalRaceId] = useState(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 && (
- {league.socialLinks.discordUrl && ( + {viewModel.socialLinks.discordUrl && ( )} - {league.socialLinks.youtubeUrl && ( + {viewModel.socialLinks.youtubeUrl && ( )} - {league.socialLinks.websiteUrl && ( + {viewModel.socialLinks.websiteUrl && ( (null); + const [league, setLeague] = useState(null); const [scoringConfig, setScoringConfig] = useState(null); const [loading, setLoading] = useState(true); const [activeSection, setActiveSection] = useState('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 { diff --git a/apps/website/app/leagues/[id]/settings/page.tsx b/apps/website/app/leagues/[id]/settings/page.tsx index efb397fba..6826d4332 100644 --- a/apps/website/app/leagues/[id]/settings/page.tsx +++ b/apps/website/app/leagues/[id]/settings/page.tsx @@ -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(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); diff --git a/apps/website/app/leagues/[id]/standings/page.tsx b/apps/website/app/leagues/[id]/standings/page.tsx index 86985c46f..d5f77c535 100644 --- a/apps/website/app/leagues/[id]/standings/page.tsx +++ b/apps/website/app/leagues/[id]/standings/page.tsx @@ -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([]); const [drivers, setDrivers] = useState([]); @@ -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(); diff --git a/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/page.tsx b/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/page.tsx index 083dfb576..7ef2367dd 100644 --- a/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/page.tsx +++ b/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/page.tsx @@ -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(null); const [race, setRace] = useState(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, diff --git a/apps/website/app/leagues/page.tsx b/apps/website/app/leagues/page.tsx index d10faf075..db7ad3515 100644 --- a/apps/website/app/leagues/page.tsx +++ b/apps/website/app/leagues/page.tsx @@ -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 { diff --git a/apps/website/app/profile/page.tsx b/apps/website/app/profile/page.tsx index 1533d1a03..aee040c92 100644 --- a/apps/website/app/profile/page.tsx +++ b/apps/website/app/profile/page.tsx @@ -12,7 +12,7 @@ import type { ProfileOverviewAchievementViewModel, ProfileOverviewSocialHandleViewModel, ProfileOverviewViewModel -} from '@core/racing/application/presenters/IProfileOverviewPresenter'; +} from '@/lib/view-models/ProfileOverviewViewModel'; import { Activity, Award, diff --git a/apps/website/app/profile/sponsorship-requests/page.tsx b/apps/website/app/profile/sponsorship-requests/page.tsx index 7555fbb42..3aa45cbbd 100644 --- a/apps/website/app/profile/sponsorship-requests/page.tsx +++ b/apps/website/app/profile/sponsorship-requests/page.tsx @@ -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({ diff --git a/apps/website/app/races/[id]/page.tsx b/apps/website/app/races/[id]/page.tsx index a645220d8..a898ee68c 100644 --- a/apps/website/app/races/[id]/page.tsx +++ b/apps/website/app/races/[id]/page.tsx @@ -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(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); diff --git a/apps/website/app/races/[id]/results/page.tsx b/apps/website/app/races/[id]/results/page.tsx index 9d8f60ef6..6941c9aaf 100644 --- a/apps/website/app/races/[id]/results/page.tsx +++ b/apps/website/app/races/[id]/results/page.tsx @@ -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(null); const [raceSOF, setRaceSOF] = useState(null); diff --git a/apps/website/app/teams/[id]/page.tsx b/apps/website/app/teams/[id]/page.tsx index 3d3677814..20e0a9202 100644 --- a/apps/website/app/teams/[id]/page.tsx +++ b/apps/website/app/teams/[id]/page.tsx @@ -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(null); - const [memberships, setMemberships] = useState([]); - const [activeTab, setActiveTab] = useState('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(null); + const [memberships, setMemberships] = useState([]); + const [activeTab, setActiveTab] = useState('overview'); + const [loading, setLoading] = useState(true); + const [isAdmin, setIsAdmin] = useState(false); + const currentDriverId = useEffectiveDriverId(); + const isSponsorMode = useSponsorMode(); const loadTeamData = useCallback(async () => { setLoading(true); diff --git a/apps/website/app/teams/leaderboard/page.tsx b/apps/website/app/teams/leaderboard/page.tsx index ca4d13244..43919e79b 100644 --- a/apps/website/app/teams/leaderboard/page.tsx +++ b/apps/website/app/teams/leaderboard/page.tsx @@ -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 { diff --git a/apps/website/app/teams/page.tsx b/apps/website/app/teams/page.tsx index 45866e348..2527b2b4a 100644 --- a/apps/website/app/teams/page.tsx +++ b/apps/website/app/teams/page.tsx @@ -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 { diff --git a/apps/website/lib/services/ServiceProvider.tsx b/apps/website/lib/services/ServiceProvider.tsx new file mode 100644 index 000000000..c2e0634bb --- /dev/null +++ b/apps/website/lib/services/ServiceProvider.tsx @@ -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(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 ( + + {children} + + ); +} + +export function useServices(): Services { + const services = useContext(ServicesContext); + if (!services) { + throw new Error('useServices must be used within a ServiceProvider'); + } + return services; +} \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueMembershipService.ts b/apps/website/lib/services/leagues/LeagueMembershipService.ts index 26432e604..28f175ec1 100644 --- a/apps/website/lib/services/leagues/LeagueMembershipService.ts +++ b/apps/website/lib/services/leagues/LeagueMembershipService.ts @@ -7,6 +7,10 @@ export class LeagueMembershipService { // In-memory cache for memberships (populated via API calls) private static leagueMemberships = new Map(); + 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 { + 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); + } } \ No newline at end of file diff --git a/apps/website/lib/services/sponsors/SponsorshipService.ts b/apps/website/lib/services/sponsors/SponsorshipService.ts index fda5e352e..ea48b2302 100644 --- a/apps/website/lib/services/sponsors/SponsorshipService.ts +++ b/apps/website/lib/services/sponsors/SponsorshipService.ts @@ -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 { 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: [] }; + } } \ No newline at end of file diff --git a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts index 9123ecb24..4caba4f00 100644 --- a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts +++ b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts @@ -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; } diff --git a/apps/website/lib/view-models/LeagueSummaryViewModel.ts b/apps/website/lib/view-models/LeagueSummaryViewModel.ts index 961617c2b..13533a428 100644 --- a/apps/website/lib/view-models/LeagueSummaryViewModel.ts +++ b/apps/website/lib/view-models/LeagueSummaryViewModel.ts @@ -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; + }; } \ No newline at end of file diff --git a/apps/website/lib/view-models/ProfileOverviewViewModel.ts b/apps/website/lib/view-models/ProfileOverviewViewModel.ts new file mode 100644 index 000000000..93f70e2f9 --- /dev/null +++ b/apps/website/lib/view-models/ProfileOverviewViewModel.ts @@ -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; +} \ No newline at end of file diff --git a/apps/website/lib/view-models/TeamSummaryViewModel.ts b/apps/website/lib/view-models/TeamSummaryViewModel.ts index 3a692091a..9789afdc4 100644 --- a/apps/website/lib/view-models/TeamSummaryViewModel.ts +++ b/apps/website/lib/view-models/TeamSummaryViewModel.ts @@ -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 */