refactor page to use services
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
ProfileOverviewAchievementViewModel,
|
||||
ProfileOverviewSocialHandleViewModel,
|
||||
ProfileOverviewViewModel
|
||||
} from '@core/racing/application/presenters/IProfileOverviewPresenter';
|
||||
} from '@/lib/view-models/ProfileOverviewViewModel';
|
||||
import {
|
||||
Activity,
|
||||
Award,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
101
apps/website/lib/services/ServiceProvider.tsx
Normal file
101
apps/website/lib/services/ServiceProvider.tsx
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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: [] };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
100
apps/website/lib/view-models/ProfileOverviewViewModel.ts
Normal file
100
apps/website/lib/view-models/ProfileOverviewViewModel.ts
Normal 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;
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user