refactor page to use services

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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