remove core from pages

This commit is contained in:
2025-12-18 19:14:50 +01:00
parent 9814d9682c
commit 4a3087ae35
35 changed files with 552 additions and 354 deletions

View File

@@ -25,7 +25,7 @@ export default function LeagueDetailPage() {
const params = useParams();
const leagueId = params.id as string;
const isSponsor = useSponsorMode();
const { leagueService, leagueMembershipService } = useServices();
const { leagueService, leagueMembershipService, raceService } = useServices();
const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null);
const [loading, setLoading] = useState(true);
@@ -168,8 +168,7 @@ export default function LeagueDetailPage() {
<Calendar className="w-4 h-4" />
<span>Started {new Date(race.date).toLocaleDateString()}</span>
</div>
{/* TODO: Add registeredCount and strengthOfField to RaceDTO */}
{/* {race.registeredCount && (
{race.registeredCount && (
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>{race.registeredCount} drivers registered</span>
@@ -180,7 +179,7 @@ export default function LeagueDetailPage() {
<Trophy className="w-4 h-4" />
<span>SOF: {race.strengthOfField}</span>
</div>
)} */}
)}
</div>
</div>
))}
@@ -482,10 +481,7 @@ export default function LeagueDetailPage() {
raceName={race.name}
onConfirm={async () => {
try {
// TODO: Use service to complete race
// const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_URL || '');
// const raceService = serviceFactory.createRaceService();
// await raceService.completeRace(race.id);
await raceService.completeRace(race.id);
await loadLeagueData();
setEndRaceModalRaceId(null);
} catch (err) {

View File

@@ -3,9 +3,8 @@
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Card from '@/components/ui/Card';
import type { LeagueScoringConfigDTO } from '@core/racing/application/dto/LeagueScoringConfigDTO';
import { useServices } from '@/lib/services/ServiceProvider';
import type { LeagueWithCapacityDTO } from '@/lib/types/generated/LeagueWithCapacityDTO';
import { LeagueDetailPageViewModel } from '@/lib/view-models/LeagueDetailPageViewModel';
type RulebookSection = 'scoring' | 'conduct' | 'protests' | 'penalties';
@@ -13,8 +12,7 @@ export default function LeagueRulebookPage() {
const params = useParams();
const leagueId = params.id as string;
const [league, setLeague] = useState<LeagueWithCapacityDTO | null>(null);
const [scoringConfig, setScoringConfig] = useState<LeagueScoringConfigDTO | null>(null);
const [viewModel, setViewModel] = useState<LeagueDetailPageViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [activeSection, setActiveSection] = useState<RulebookSection>('scoring');
@@ -22,14 +20,13 @@ export default function LeagueRulebookPage() {
async function loadData() {
try {
const { leagueService } = useServices();
const viewModel = await leagueService.getLeagueDetailPageData(leagueId);
if (!viewModel) {
const data = await leagueService.getLeagueDetailPageData(leagueId);
if (!data) {
setLoading(false);
return;
}
setLeague(viewModel.league);
setScoringConfig(viewModel.scoringConfig);
setViewModel(data);
} catch (err) {
console.error('Failed to load scoring config:', err);
} finally {
@@ -48,7 +45,7 @@ export default function LeagueRulebookPage() {
);
}
if (!league || !scoringConfig) {
if (!viewModel || !viewModel.scoringConfig) {
return (
<Card>
<div className="text-center py-12 text-gray-400">Unable to load rulebook</div>
@@ -56,7 +53,7 @@ export default function LeagueRulebookPage() {
);
}
const primaryChampionship = scoringConfig.championships.find(c => c.type === 'driver') ?? scoringConfig.championships[0];
const primaryChampionship = viewModel.scoringConfig.championships.find(c => c.type === 'driver') ?? viewModel.scoringConfig.championships[0];
const positionPoints = primaryChampionship?.pointsPreview
.filter(p => p.sessionType === primaryChampionship.sessionTypes[0])
.map(p => ({ position: p.position, points: p.points }))
@@ -78,7 +75,7 @@ export default function LeagueRulebookPage() {
<p className="text-sm text-gray-400 mt-1">Official rules and regulations</p>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary-blue/10 border border-primary-blue/20">
<span className="text-sm font-medium text-primary-blue">{scoringConfig.scoringPresetName || 'Custom Rules'}</span>
<span className="text-sm font-medium text-primary-blue">{viewModel.scoringConfig.scoringPresetName || 'Custom Rules'}</span>
</div>
</div>
@@ -106,11 +103,11 @@ export default function LeagueRulebookPage() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Platform</p>
<p className="text-lg font-semibold text-white">{scoringConfig.gameName}</p>
<p className="text-lg font-semibold text-white">{viewModel.scoringConfig.gameName}</p>
</div>
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Championships</p>
<p className="text-lg font-semibold text-white">{scoringConfig.championships.length}</p>
<p className="text-lg font-semibold text-white">{viewModel.scoringConfig.championships.length}</p>
</div>
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Sessions Scored</p>
@@ -120,8 +117,8 @@ export default function LeagueRulebookPage() {
</div>
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Drop Policy</p>
<p className="text-lg font-semibold text-white truncate" title={scoringConfig.dropPolicySummary}>
{scoringConfig.dropPolicySummary.includes('All') ? 'None' : 'Active'}
<p className="text-lg font-semibold text-white truncate" title={viewModel.scoringConfig.dropPolicySummary}>
{viewModel.scoringConfig.dropPolicySummary.includes('All') ? 'None' : 'Active'}
</p>
</div>
</div>
@@ -192,10 +189,10 @@ export default function LeagueRulebookPage() {
)}
{/* Drop Policy */}
{!scoringConfig.dropPolicySummary.includes('All results count') && (
{!viewModel.scoringConfig.dropPolicySummary.includes('All results count') && (
<Card>
<h2 className="text-lg font-semibold text-white mb-4">Drop Policy</h2>
<p className="text-sm text-gray-300">{scoringConfig.dropPolicySummary}</p>
<p className="text-sm text-gray-300">{viewModel.scoringConfig.dropPolicySummary}</p>
<p className="text-xs text-gray-500 mt-3">
Drop rules are applied automatically when calculating championship standings.
</p>

View File

@@ -4,6 +4,7 @@ import LeagueSchedule from '@/components/leagues/LeagueSchedule';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { useServices } from '@/lib/services/ServiceProvider';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -12,17 +13,18 @@ export default function LeagueSchedulePage() {
const router = useRouter();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const { leagueMembershipService } = useServices();
const [isAdmin, setIsAdmin] = useState(false);
const [showCreateForm, setShowCreateForm] = useState(false);
useEffect(() => {
async function checkAdmin() {
const membershipRepo = getLeagueMembershipRepository();
const membership = await membershipRepo.getMembership(leagueId, currentDriverId);
await leagueMembershipService.fetchLeagueMemberships(leagueId);
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
}
checkAdmin();
}, [leagueId, currentDriverId]);
}, [leagueId, currentDriverId, leagueMembershipService]);
return (
<div className="space-y-6">

View File

@@ -1,6 +0,0 @@
import { redirect } from 'next/navigation';
export default async function ScoringPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
redirect(`/leagues/${id}/rulebook`);
}

View File

@@ -8,7 +8,6 @@ import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
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';
import { AlertTriangle, Settings, UserCog } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation';
@@ -170,8 +169,8 @@ export default function LeagueSettingsPage() {
>
<option value="">Select new owner...</option>
{settings.members.map((member) => (
<option key={member.id} value={member.id}>
{member.name}
<option key={member.driver.id} value={member.driver.id}>
{member.driver.name}
</option>
))}
</select>

View File

@@ -4,7 +4,8 @@ import { LeagueSponsorshipsSection } from '@/components/leagues/LeagueSponsorshi
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import type { League } from '@core/racing/domain/entities/League';
import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueDetailViewModel } from '@/lib/view-models/LeagueDetailViewModel';
import { AlertTriangle, Building } from 'lucide-react';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -13,23 +14,23 @@ export default function LeagueSponsorshipsPage() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const { leagueService, leagueMembershipService } = useServices();
const [league, setLeague] = useState<League | null>(null);
const [league, setLeague] = useState<LeagueDetailViewModel | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadData() {
try {
const leagueRepo = getLeagueRepository();
const membershipRepo = getLeagueMembershipRepository();
const [leagueData, membership] = await Promise.all([
leagueRepo.findById(leagueId),
membershipRepo.getMembership(leagueId, currentDriverId),
const [leagueDetail, memberships] = await Promise.all([
leagueService.getLeagueDetail(leagueId, currentDriverId),
leagueMembershipService.fetchLeagueMemberships(leagueId),
]);
setLeague(leagueData);
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
setLeague(leagueDetail);
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
} catch (err) {
console.error('Failed to load league:', err);
@@ -39,7 +40,7 @@ export default function LeagueSponsorshipsPage() {
}
loadData();
}, [leagueId, currentDriverId]);
}, [leagueId, currentDriverId, leagueService, leagueMembershipService]);
if (loading) {
return (

View File

@@ -3,9 +3,10 @@
import StandingsTable from '@/components/leagues/StandingsTable';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import type { DriverDto, LeagueMembership } from '@/lib/dtos';
import type { LeagueMembership, MembershipRole } from '@/lib/types';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { useServices } from '@/lib/services/ServiceProvider';
import { DriverViewModel } from '@/lib/view-models';
import type { LeagueStandingsViewModel } from '@/lib/view-models';
import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
import { useParams } from 'next/navigation';
@@ -18,7 +19,7 @@ export default function LeagueStandingsPage() {
const { leagueService } = useServices();
const [standings, setStandings] = useState<StandingEntryViewModel[]>([]);
const [drivers, setDrivers] = useState<DriverDto[]>([]);
const [drivers, setDrivers] = useState<DriverViewModel[]>([]);
const [memberships, setMemberships] = useState<LeagueMembership[]>([]);
const [viewModel, setViewModel] = useState<LeagueStandingsViewModel | null>(null);
const [loading, setLoading] = useState(true);
@@ -30,7 +31,7 @@ export default function LeagueStandingsPage() {
const vm = await leagueService.getLeagueStandings(leagueId, currentDriverId);
setViewModel(vm);
setStandings(vm.standings);
setDrivers(vm.drivers);
setDrivers(vm.drivers.map(d => new DriverViewModel(d)));
setMemberships(vm.memberships);
// Check if current user is admin
@@ -53,21 +54,7 @@ export default function LeagueStandingsPage() {
}
try {
const membershipRepo = getLeagueMembershipRepository();
const performer = await membershipRepo.getMembership(leagueId, currentDriverId);
if (!performer || (performer.role !== 'owner' && performer.role !== 'admin')) {
throw new Error('Only owners or admins can remove members');
}
const membership = await membershipRepo.getMembership(leagueId, driverId);
if (!membership) {
throw new Error('Member not found');
}
if (membership.role === 'owner') {
throw new Error('Cannot remove the league owner');
}
await membershipRepo.removeMembership(leagueId, driverId);
await leagueService.removeMember(leagueId, currentDriverId, driverId);
await loadData();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to remove member');
@@ -76,25 +63,7 @@ export default function LeagueStandingsPage() {
const handleUpdateRole = async (driverId: string, newRole: MembershipRole) => {
try {
const membershipRepo = getLeagueMembershipRepository();
const performer = await membershipRepo.getMembership(leagueId, currentDriverId);
if (!performer || performer.role !== 'owner') {
throw new Error('Only the league owner can update roles');
}
const membership = await membershipRepo.getMembership(leagueId, driverId);
if (!membership) {
throw new Error('Member not found');
}
if (membership.role === 'owner') {
throw new Error('Cannot change the owner role');
}
await membershipRepo.saveMembership({
...membership,
role: newRole,
});
await leagueService.updateMemberRole(leagueId, currentDriverId, driverId, newRole);
await loadData();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update role');

View File

@@ -6,12 +6,8 @@ import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
import { EntityMappers } from '@core/racing/application/mappers/EntityMappers';
import type { Penalty, PenaltyType } from '@core/racing/domain/entities/Penalty';
import type { Protest } from '@core/racing/domain/entities/Protest';
import type { Race } from '@core/racing/domain/entities/Race';
import { useServices } from '@/lib/services/ServiceProvider';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import {
AlertCircle,
AlertTriangle,
@@ -28,97 +24,98 @@ import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
// Local type definitions to replace core imports
type PenaltyType = 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points';
type DriverDTO = {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
};
interface RaceWithProtests {
race: Race;
pendingProtests: Protest[];
resolvedProtests: Protest[];
penalties: Penalty[];
race: any;
pendingProtests: any[];
resolvedProtests: any[];
penalties: any[];
}
export default function LeagueStewardingPage() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const { raceService, protestService, driverService, leagueMembershipService, penaltyService } = useServices();
const [races, setRaces] = useState<Race[]>([]);
const [protestsByRace, setProtestsByRace] = useState<Record<string, Protest[]>>({});
const [penaltiesByRace, setPenaltiesByRace] = useState<Record<string, Penalty[]>>({});
const [races, setRaces] = useState<any[]>([]);
const [protestsByRace, setProtestsByRace] = useState<Record<string, any[]>>({});
const [penaltiesByRace, setPenaltiesByRace] = useState<Record<string, any[]>>({});
const [driversById, setDriversById] = useState<Record<string, DriverDTO>>({});
const [allDrivers, setAllDrivers] = useState<DriverDTO[]>([]);
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending');
const [selectedProtest, setSelectedProtest] = useState<Protest | null>(null);
const [selectedProtest, setSelectedProtest] = useState<any | null>(null);
const [expandedRaces, setExpandedRaces] = useState<Set<string>>(new Set());
const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false);
useEffect(() => {
async function checkAdmin() {
const membershipRepo = getLeagueMembershipRepository();
const membership = await membershipRepo.getMembership(leagueId, currentDriverId);
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
const membership = await leagueMembershipService.getMembership(leagueId, currentDriverId);
setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false);
}
checkAdmin();
}, [leagueId, currentDriverId]);
}, [leagueId, currentDriverId, leagueMembershipService]);
useEffect(() => {
async function loadData() {
setLoading(true);
try {
const raceRepo = getRaceRepository();
const protestRepo = getProtestRepository();
const penaltyRepo = getPenaltyRepository();
const driverRepo = getDriverRepository();
// Get all races for this league
const leagueRaces = await raceRepo.findByLeagueId(leagueId);
const leagueRaces = await raceService.findByLeagueId(leagueId);
setRaces(leagueRaces);
// Get protests and penalties for each race
const protestsMap: Record<string, Protest[]> = {};
const penaltiesMap: Record<string, Penalty[]> = {};
const protestsMap: Record<string, any[]> = {};
const penaltiesMap: Record<string, any[]> = {};
const driverIds = new Set<string>();
for (const race of leagueRaces) {
const raceProtests = await protestRepo.findByRaceId(race.id);
const racePenalties = await penaltyRepo.findByRaceId(race.id);
const raceProtests = await protestService.findByRaceId(race.id);
const racePenalties = await penaltyService.findByRaceId(race.id);
protestsMap[race.id] = raceProtests;
penaltiesMap[race.id] = racePenalties;
// Collect driver IDs
raceProtests.forEach((p) => {
raceProtests.forEach((p: any) => {
driverIds.add(p.protestingDriverId);
driverIds.add(p.accusedDriverId);
});
racePenalties.forEach((p) => {
racePenalties.forEach((p: any) => {
driverIds.add(p.driverId);
});
}
setProtestsByRace(protestsMap);
setPenaltiesByRace(penaltiesMap);
// Load driver info
const driverEntities = await Promise.all(
Array.from(driverIds).map((id) => driverRepo.findById(id)),
);
const byId: Record<string, DriverDTO> = {};
const driverEntities = await driverService.findByIds(Array.from(driverIds));
const byId: Record<string, any> = {};
driverEntities.forEach((driver) => {
if (driver) {
const dto = EntityMappers.toDriverDTO(driver);
if (dto) {
byId[dto.id] = dto;
}
byId[driver.id] = driver;
}
});
setDriversById(byId);
setAllDrivers(Object.values(byId));
// Auto-expand races with pending protests
const racesWithPending = new Set<string>();
Object.entries(protestsMap).forEach(([raceId, protests]) => {
if (protests.some(p => p.status === 'pending' || p.status === 'under_review')) {
if (protests.some((p: any) => p.status === 'pending' || p.status === 'under_review')) {
racesWithPending.add(raceId);
}
});
@@ -133,7 +130,7 @@ export default function LeagueStewardingPage() {
if (isAdmin) {
loadData();
}
}, [leagueId, isAdmin]);
}, [leagueId, isAdmin, raceService, protestService, driverService, penaltyService]);
// Compute race data with protest/penalty info
const racesWithData = useMemo((): RaceWithProtests[] => {
@@ -168,10 +165,7 @@ export default function LeagueStewardingPage() {
penaltyValue: number,
stewardNotes: string
) => {
const reviewUseCase = getReviewProtestUseCase();
const penaltyUseCase = getApplyPenaltyUseCase();
await reviewUseCase.execute({
await protestService.reviewProtest({
protestId,
stewardId: currentDriverId,
decision: 'uphold',
@@ -179,14 +173,14 @@ export default function LeagueStewardingPage() {
});
// Find the protest
let foundProtest: Protest | undefined;
let foundProtest: any | undefined;
Object.values(protestsByRace).forEach(protests => {
const p = protests.find(pr => pr.id === protestId);
if (p) foundProtest = p;
});
if (foundProtest) {
await penaltyUseCase.execute({
await penaltyService.applyPenalty({
raceId: foundProtest.raceId,
driverId: foundProtest.accusedDriverId,
stewardId: currentDriverId,
@@ -200,9 +194,7 @@ export default function LeagueStewardingPage() {
};
const handleRejectProtest = async (protestId: string, stewardNotes: string) => {
const reviewUseCase = getReviewProtestUseCase();
await reviewUseCase.execute({
await protestService.reviewProtest({
protestId,
stewardId: currentDriverId,
decision: 'dismiss',
@@ -210,10 +202,6 @@ export default function LeagueStewardingPage() {
});
};
const handleProtestReviewed = () => {
setSelectedProtest(null);
window.location.reload();
};
const toggleRaceExpanded = (raceId: string) => {
setExpandedRaces(prev => {

View File

@@ -3,12 +3,12 @@
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useServices } from '@/lib/services/ServiceProvider';
import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
import { RaceViewModel } from '@/lib/view-models/RaceViewModel';
import { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel';
import { ProtestDecisionCommandModel, type PenaltyType } from '@/lib/command-models/protests/ProtestDecisionCommandModel';
import type { DriverSummaryDTO } from '@/lib/types/generated/LeagueAdminProtestsDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import {
AlertCircle,
AlertTriangle,
@@ -40,7 +40,7 @@ interface TimelineEvent {
id: string;
type: 'protest_filed' | 'defense_requested' | 'defense_submitted' | 'steward_comment' | 'decision' | 'penalty_applied';
timestamp: Date;
actor: DriverDTO | null;
actor: ProtestDriverViewModel | null;
content: string;
metadata?: Record<string, unknown>;
}
@@ -114,12 +114,12 @@ export default function ProtestReviewPage() {
const leagueId = params.id as string;
const protestId = params.protestId as string;
const currentDriverId = useEffectiveDriverId();
const { protestService } = useServices();
const { protestService, leagueMembershipService } = useServices();
const [protest, setProtest] = useState<ProtestViewModel | null>(null);
const [race, setRace] = useState<RaceDTO | null>(null);
const [protestingDriver, setProtestingDriver] = useState<DriverSummaryDTO | null>(null);
const [accusedDriver, setAccusedDriver] = useState<DriverSummaryDTO | null>(null);
const [race, setRace] = useState<RaceViewModel | null>(null);
const [protestingDriver, setProtestingDriver] = useState<ProtestDriverViewModel | null>(null);
const [accusedDriver, setAccusedDriver] = useState<ProtestDriverViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
@@ -136,12 +136,12 @@ export default function ProtestReviewPage() {
useEffect(() => {
async function checkAdmin() {
const membershipRepo = getLeagueMembershipRepository();
const membership = await membershipRepo.getMembership(leagueId, currentDriverId);
setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
await leagueMembershipService.fetchLeagueMemberships(leagueId);
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false);
}
checkAdmin();
}, [leagueId, currentDriverId]);
}, [leagueId, currentDriverId, leagueMembershipService]);
useEffect(() => {
async function loadProtest() {
@@ -188,19 +188,19 @@ export default function ProtestReviewPage() {
}
];
// TODO: Add decision event when status/decisions are available in DTO
// if (protest.status === 'upheld' || protest.status === 'dismissed') {
// events.push({
// id: 'decision',
// type: 'decision',
// timestamp: protest.reviewedAt ? new Date(protest.reviewedAt) : new Date(),
// actor: null, // Would need to load steward driver
// content: protest.decisionNotes || (protest.status === 'upheld' ? 'Protest upheld' : 'Protest dismissed'),
// metadata: {
// decision: protest.status
// }
// });
// }
// Add decision event when status/decisions are available in view model
if (protest.status === 'upheld' || protest.status === 'dismissed') {
events.push({
id: 'decision',
type: 'decision',
timestamp: protest.reviewedAt ? new Date(protest.reviewedAt) : new Date(),
actor: null, // Would need to load steward driver
content: protest.decisionNotes || (protest.status === 'upheld' ? 'Protest upheld' : 'Protest dismissed'),
metadata: {
decision: protest.status
}
});
}
return events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
}, [protest, protestingDriver]);
@@ -315,9 +315,9 @@ export default function ProtestReviewPage() {
);
}
const statusConfig = getStatusConfig('pending'); // TODO: Update when status is available
const statusConfig = getStatusConfig(protest.status);
const StatusIcon = statusConfig.icon;
const isPending = true; // TODO: Update when status is available
const isPending = protest.status === 'pending';
const daysSinceFiled = Math.floor((Date.now() - new Date(protest.submittedAt).getTime()) / (1000 * 60 * 60 * 24));
return (
@@ -404,7 +404,7 @@ export default function ProtestReviewPage() {
</div>
<div className="flex items-center gap-2 text-sm">
<Calendar className="w-4 h-4 text-gray-500" />
<span className="text-gray-300">{new Date(race.date).toLocaleDateString()}</span>
<span className="text-gray-300">{race.formattedDate}</span>
</div>
{/* TODO: Add lap info when available */}
{/* <div className="flex items-center gap-2 text-sm">

View File

@@ -400,7 +400,6 @@ export default function LeaguesPage() {
}
};
// Use only real leagues from repository
const leagues = realLeagues;
const handleLeagueClick = (leagueId: string) => {

View File

@@ -3,13 +3,14 @@
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import type { League } from '@core/racing/domain/entities/League';
import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import { useServices } from '@/lib/services/ServiceProvider';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import Link from 'next/link';
import { useEffect, useState } from 'react';
interface LeagueWithRole {
league: League;
league: LeagueSummaryViewModel;
membership: LeagueMembership;
}
@@ -19,6 +20,7 @@ export default function ManageLeaguesPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const effectiveDriverId = useEffectiveDriverId();
const { leagueService, leagueMembershipService } = useServices();
useEffect(() => {
let cancelled = false;
@@ -26,14 +28,12 @@ export default function ManageLeaguesPage() {
const load = async () => {
setLoading(true);
try {
const leagueRepo = getLeagueRepository();
const membershipRepo = getLeagueMembershipRepository();
const leagues = await leagueRepo.findAll();
const leagues = await leagueService.getAllLeagues();
const memberships = await Promise.all(
leagues.map(async (league) => {
const membership = await membershipRepo.getMembership(league.id, effectiveDriverId);
await leagueMembershipService.fetchLeagueMemberships(league.id);
const membership = leagueMembershipService.getMembership(league.id, effectiveDriverId);
return { league, membership };
}),
);
@@ -76,7 +76,7 @@ export default function ManageLeaguesPage() {
return () => {
cancelled = true;
};
}, [effectiveDriverId]);
}, [effectiveDriverId, leagueService, leagueMembershipService]);
if (loading) {
return (

View File

@@ -7,12 +7,12 @@ import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
import { useServices } from '@/lib/services/ServiceProvider';
import type {
ProfileOverviewAchievementViewModel,
ProfileOverviewSocialHandleViewModel,
ProfileOverviewViewModel
} from '@/lib/view-models/ProfileOverviewViewModel';
DriverProfileAchievementViewModel,
DriverProfileSocialHandleViewModel,
DriverProfileViewModel
} from '@/lib/view-models/DriverProfileViewModel';
import {
Activity,
Award,
@@ -67,7 +67,7 @@ function getCountryFlag(countryCode: string): string {
return '🏁';
}
function getRarityColor(rarity: ProfileOverviewAchievementViewModel['rarity']) {
function getRarityColor(rarity: DriverProfileAchievementViewModel['rarity']) {
switch (rarity) {
case 'common':
return 'text-gray-400 bg-gray-400/10 border-gray-400/30';
@@ -80,7 +80,7 @@ function getRarityColor(rarity: ProfileOverviewAchievementViewModel['rarity']) {
}
}
function getAchievementIcon(icon: ProfileOverviewAchievementViewModel['icon']) {
function getAchievementIcon(icon: DriverProfileAchievementViewModel['icon']) {
switch (icon) {
case 'trophy':
return Trophy;
@@ -97,7 +97,7 @@ function getAchievementIcon(icon: ProfileOverviewAchievementViewModel['icon']) {
}
}
function getSocialIcon(platform: ProfileOverviewSocialHandleViewModel['platform']) {
function getSocialIcon(platform: DriverProfileSocialHandleViewModel['platform']) {
switch (platform) {
case 'twitter':
return Twitter;
@@ -110,7 +110,7 @@ function getSocialIcon(platform: ProfileOverviewSocialHandleViewModel['platform'
}
}
function getSocialColor(platform: ProfileOverviewSocialHandleViewModel['platform']) {
function getSocialColor(platform: DriverProfileSocialHandleViewModel['platform']) {
switch (platform) {
case 'twitter':
return 'hover:text-sky-400 hover:bg-sky-400/10';
@@ -256,12 +256,13 @@ export default function ProfilePage() {
const router = useRouter();
const searchParams = useSearchParams();
const tabParam = searchParams.get('tab') as ProfileTab | null;
const [driver, setDriver] = useState<DriverDTO | null>(null);
const { driverService, mediaService } = useServices();
const [profileData, setProfileData] = useState<DriverProfileViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
const [profileData, setProfileData] = useState<ProfileOverviewViewModel | null>(null);
const [friendRequestSent, setFriendRequestSent] = useState(false);
const effectiveDriverId = useEffectiveDriverId();
@@ -271,24 +272,8 @@ export default function ProfilePage() {
const loadData = async () => {
try {
const currentDriverId = effectiveDriverId;
// Use GetProfileOverviewUseCase to load all profile data
const profileUseCase = getGetProfileOverviewUseCase();
const profileViewModel = await profileUseCase.execute({ driverId: currentDriverId });
if (profileViewModel && profileViewModel.currentDriver) {
// Set driver from ViewModel instead of direct repository access
const driverData: DriverDTO = {
id: profileViewModel.currentDriver.id,
name: profileViewModel.currentDriver.name,
iracingId: profileViewModel.currentDriver.iracingId ?? '',
country: profileViewModel.currentDriver.country,
bio: profileViewModel.currentDriver.bio || '',
joinedAt: profileViewModel.currentDriver.joinedAt,
};
setDriver(driverData);
setProfileData(profileViewModel);
}
const profileViewModel = await driverService.getDriverProfile(currentDriverId);
setProfileData(profileViewModel);
} catch (error) {
console.error('Failed to load profile:', error);
} finally {
@@ -296,7 +281,7 @@ export default function ProfilePage() {
}
};
void loadData();
}, [effectiveDriverId]);
}, [effectiveDriverId, driverService]);
// Update URL when tab changes
useEffect(() => {
@@ -319,24 +304,13 @@ export default function ProfilePage() {
}
}, [tabParam]);
const handleSaveSettings = async (updates: Partial<DriverDTO>) => {
if (!driver) return;
const handleSaveSettings = async (updates: { bio?: string; country?: string }) => {
if (!profileData?.currentDriver) return;
try {
const updateProfileUseCase = getUpdateDriverProfileUseCase();
const input: { driverId: string; bio?: string; country?: string } = { driverId: driver.id };
if (typeof updates.bio === 'string') {
input.bio = updates.bio;
}
if (typeof updates.country === 'string') {
input.country = updates.country;
}
const updatedDto = await updateProfileUseCase.execute(input);
if (updatedDto) {
setDriver(updatedDto);
setEditMode(false);
}
const updatedProfile = await driverService.updateProfile(updates);
setProfileData(updatedProfile);
setEditMode(false);
} catch (error) {
console.error('Failed to update profile:', error);
}
@@ -360,7 +334,7 @@ export default function ProfilePage() {
);
}
if (!driver) {
if (!profileData?.currentDriver) {
return (
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-8">
@@ -387,12 +361,12 @@ export default function ProfilePage() {
}
// Extract data from profileData ViewModel
const currentDriver = profileData?.currentDriver || null;
const stats = profileData?.stats || null;
const finishDistribution = profileData?.finishDistribution || null;
const teamMemberships = profileData?.teamMemberships || [];
const socialSummary = profileData?.socialSummary || { friendsCount: 0, friends: [] };
const extendedProfile = profileData?.extendedProfile;
const currentDriver = profileData.currentDriver;
const stats = profileData.stats;
const finishDistribution = profileData.finishDistribution;
const teamMemberships = profileData.teamMemberships;
const socialSummary = profileData.socialSummary;
const extendedProfile = profileData.extendedProfile;
const globalRank = currentDriver?.globalRank || null;
// Show edit mode
@@ -405,7 +379,7 @@ export default function ProfilePage() {
Cancel
</Button>
</div>
<ProfileSettings driver={driver} onSave={handleSaveSettings} />
<ProfileSettings driver={profileData.currentDriver} onSave={handleSaveSettings} />
</div>
);
}
@@ -428,8 +402,8 @@ export default function ProfilePage() {
<div className="w-28 h-28 md:w-36 md:h-36 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-1 shadow-xl shadow-primary-blue/20">
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
<Image
src={getImageService().getDriverAvatar(driver.id)}
alt={driver.name}
src={mediaService.getDriverAvatar(currentDriver.id)}
alt={currentDriver.name}
width={144}
height={144}
className="w-full h-full object-cover"
@@ -443,9 +417,9 @@ export default function ProfilePage() {
{/* Driver Info */}
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-3 mb-2">
<h1 className="text-3xl md:text-4xl font-bold text-white">{driver.name}</h1>
<span className="text-4xl" aria-label={`Country: ${driver.country}`}>
{getCountryFlag(driver.country)}
<h1 className="text-3xl md:text-4xl font-bold text-white">{currentDriver.name}</h1>
<span className="text-4xl" aria-label={`Country: ${currentDriver.country}`}>
{getCountryFlag(currentDriver.country)}
</span>
{teamMemberships.length > 0 && teamMemberships[0] && (
<span className="px-3 py-1 bg-purple-600/20 text-purple-400 rounded-full text-sm font-semibold border border-purple-600/30">
@@ -488,11 +462,11 @@ export default function ProfilePage() {
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-400">
<span className="flex items-center gap-1.5">
<Globe className="w-4 h-4" />
iRacing: {driver.iracingId}
iRacing: {currentDriver.iracingId}
</span>
<span className="flex items-center gap-1.5">
<Calendar className="w-4 h-4" />
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
Joined {new Date(currentDriver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</span>
{extendedProfile && (
<span className="flex items-center gap-1.5">
@@ -564,13 +538,13 @@ export default function ProfilePage() {
</div>
{/* Bio Section */}
{driver.bio && (
{currentDriver.bio && (
<Card>
<h2 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
<User className="w-5 h-5 text-primary-blue" />
About
</h2>
<p className="text-gray-300 leading-relaxed">{driver.bio}</p>
<p className="text-gray-300 leading-relaxed">{currentDriver.bio}</p>
</Card>
)}
@@ -910,7 +884,7 @@ export default function ProfilePage() {
>
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
<Image
src={getImageService().getDriverAvatar(friend.id)}
src={mediaService.getDriverAvatar(friend.id)}
alt={friend.name}
width={32}
height={32}
@@ -932,13 +906,13 @@ export default function ProfilePage() {
</>
)}
{activeTab === 'history' && driver && (
{activeTab === 'history' && currentDriver && (
<Card>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<History className="w-5 h-5 text-red-400" />
Race History
</h2>
<ProfileRaceHistory driverId={driver.id} />
<ProfileRaceHistory driverId={currentDriver.id} />
</Card>
)}

View File

@@ -1,15 +1,16 @@
'use client';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import PendingSponsorshipRequests, { type PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
import PendingSponsorshipRequests from '@/components/sponsors/PendingSponsorshipRequests';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useServices } from '@/lib/services/ServiceProvider';
import { SponsorshipRequestViewModel } from '@/lib/view-models/SponsorshipRequestViewModel';
import { AlertTriangle, Building, ChevronRight, Handshake, Trophy, User, Users } from 'lucide-react';
import Link from 'next/link';
@@ -17,7 +18,7 @@ interface EntitySection {
entityType: 'driver' | 'team' | 'race' | 'season';
entityId: string;
entityName: string;
requests: PendingRequestDTO[];
requests: SponsorshipRequestViewModel[];
}
export default function SponsorshipRequestsPage() {
@@ -33,50 +34,45 @@ export default function SponsorshipRequestsPage() {
setError(null);
try {
const { sponsorshipService } = useServices();
const driverRepo = getDriverRepository();
const leagueRepo = getLeagueRepository();
const teamRepo = getTeamRepository();
const leagueMembershipRepo = getLeagueMembershipRepository();
const teamMembershipRepo = getTeamMembershipRepository();
const { sponsorshipService, driverService, leagueService, teamService, leagueMembershipService } = useServices();
const allSections: EntitySection[] = [];
// 1. Driver's own sponsorship requests
const driverResult = await sponsorshipService.getPendingSponsorshipRequests({
const driverRequests = await sponsorshipService.getPendingSponsorshipRequests({
entityType: 'driver',
entityId: currentDriverId,
});
if (driverResult && driverResult.requests.length > 0) {
const driver = await driverRepo.findById(currentDriverId);
if (driverRequests.length > 0) {
const driverProfile = await driverService.getDriverProfile(currentDriverId);
allSections.push({
entityType: 'driver',
entityId: currentDriverId,
entityName: driver?.name ?? 'Your Profile',
requests: driverResult.requests,
entityName: driverProfile?.currentDriver?.name ?? 'Your Profile',
requests: driverRequests,
});
}
// 2. Leagues where the user is admin/owner
const allLeagues = await leagueRepo.findAll();
const allLeagues = await leagueService.getAllLeagues();
for (const league of allLeagues) {
const membership = await leagueMembershipRepo.getMembership(league.id, currentDriverId);
if (membership && isLeagueAdminOrHigherRole(membership.role)) {
const membership = await leagueMembershipService.getMembership(league.id, currentDriverId);
if (membership && LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role)) {
// 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 leagueResult = await sponsorshipService.getPendingSponsorshipRequests({
const leagueRequests = await sponsorshipService.getPendingSponsorshipRequests({
entityType: 'season',
entityId: league.id, // Using league ID as a proxy for now
});
if (leagueResult && leagueResult.requests.length > 0) {
if (leagueRequests.length > 0) {
allSections.push({
entityType: 'season',
entityId: league.id,
entityName: league.name,
requests: leagueResult.requests,
requests: leagueRequests,
});
}
} catch (err) {
@@ -84,28 +80,28 @@ export default function SponsorshipRequestsPage() {
}
}
}
// 3. Teams where the user is owner/manager
const allTeams = await teamRepo.findAll();
const allTeams = await teamService.getAllTeams();
for (const team of allTeams) {
const membership = await teamMembershipRepo.getMembership(team.id, currentDriverId);
const membership = await teamService.getMembership(team.id, currentDriverId);
if (membership && (membership.role === 'owner' || membership.role === 'manager')) {
const teamResult = await sponsorshipService.getPendingSponsorshipRequests({
const teamRequests = await sponsorshipService.getPendingSponsorshipRequests({
entityType: 'team',
entityId: team.id,
});
if (teamResult && teamResult.requests.length > 0) {
if (teamRequests.length > 0) {
allSections.push({
entityType: 'team',
entityId: team.id,
entityName: team.name,
requests: teamResult.requests,
requests: teamRequests,
});
}
}
}
setSections(allSections);
} catch (err) {
console.error('Failed to load sponsorship requests:', err);
@@ -120,24 +116,14 @@ export default function SponsorshipRequestsPage() {
}, [loadAllRequests]);
const handleAccept = async (requestId: string) => {
const useCase = getAcceptSponsorshipRequestUseCase();
await useCase.execute({
requestId,
respondedBy: currentDriverId,
});
const { sponsorshipService } = useServices();
await sponsorshipService.acceptSponsorshipRequest(requestId, currentDriverId);
await loadAllRequests();
};
const handleReject = async (requestId: string, reason?: string) => {
const useCase = getRejectSponsorshipRequestUseCase();
const input: { requestId: string; respondedBy: string; reason?: string } = {
requestId,
respondedBy: currentDriverId,
};
if (typeof reason === 'string') {
input.reason = reason;
}
await useCase.execute(input);
const { sponsorshipService } = useServices();
await sponsorshipService.rejectSponsorshipRequest(requestId, currentDriverId, reason);
await loadAllRequests();
};