'use client'; import { useState, useEffect, useCallback } from 'react'; import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import Button from '../ui/Button'; import Card from '../ui/Card'; import LeagueMembers from './LeagueMembers'; import ScheduleRaceForm from './ScheduleRaceForm'; import { loadLeagueJoinRequests, approveLeagueJoinRequest, rejectLeagueJoinRequest, loadLeagueOwnerSummary, loadLeagueConfig, loadLeagueProtests, removeLeagueMember as removeLeagueMemberCommand, updateLeagueMemberRole as updateLeagueMemberRoleCommand, type LeagueJoinRequestViewModel, type LeagueOwnerSummaryViewModel, type LeagueAdminProtestsViewModel, } from '@/lib/presenters/LeagueAdminPresenter'; import type { LeagueConfigFormModel } from '@gridpilot/racing/application'; import type { LeagueSummaryViewModel } from '@/lib/presenters/LeagueAdminPresenter'; import { LeagueBasicsSection } from './LeagueBasicsSection'; import { LeagueStructureSection } from './LeagueStructureSection'; import { LeagueScoringSection } from './LeagueScoringSection'; import { LeagueDropSection } from './LeagueDropSection'; import { LeagueTimingsSection } from './LeagueTimingsSection'; import { LeagueSponsorshipsSection } from './LeagueSponsorshipsSection'; import { LeagueMembershipFeesSection } from './LeagueMembershipFeesSection'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import type { MembershipRole } from '@/lib/leagueMembership'; import DriverSummaryPill from '@/components/profile/DriverSummaryPill'; import DriverIdentity from '@/components/drivers/DriverIdentity'; import Modal from '@/components/ui/Modal'; import { AlertTriangle, CheckCircle, Clock, XCircle, Flag, Calendar, User, DollarSign, Wallet, Paintbrush, Trophy, Download, Car, Upload } from 'lucide-react'; type JoinRequest = LeagueJoinRequestViewModel; interface LeagueAdminProps { league: LeagueSummaryViewModel; onLeagueUpdate?: () => void; } export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps) { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); const currentDriverId = useEffectiveDriverId(); const [joinRequests, setJoinRequests] = useState([]); const [ownerSummary, setOwnerSummary] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'members' | 'requests' | 'races' | 'settings' | 'protests' | 'sponsorships' | 'fees' | 'wallet' | 'prizes' | 'liveries'>('members'); const [downloadingLiveryPack, setDownloadingLiveryPack] = useState(false); const [rejectReason, setRejectReason] = useState(''); const [configForm, setConfigForm] = useState(null); const [configLoading, setConfigLoading] = useState(false); const [protestsViewModel, setProtestsViewModel] = useState(null); const [protestsLoading, setProtestsLoading] = useState(false); const loadJoinRequests = useCallback(async () => { setLoading(true); try { const requests = await loadLeagueJoinRequests(league.id); setJoinRequests(requests); } catch (err) { console.error('Failed to load join requests:', err); } finally { setLoading(false); } }, [league.id]); useEffect(() => { loadJoinRequests(); }, [loadJoinRequests]); useEffect(() => { async function loadOwner() { try { const summary = await loadLeagueOwnerSummary({ ownerId: league.ownerId }); setOwnerSummary(summary); } catch (err) { console.error('Failed to load league owner:', err); } } loadOwner(); }, [league]); useEffect(() => { async function loadConfig() { setConfigLoading(true); try { const configVm = await loadLeagueConfig(league.id); setConfigForm(configVm.form); } catch (err) { console.error('Failed to load league config:', err); } finally { setConfigLoading(false); } } loadConfig(); }, [league.id]); // Load protests for this league's races useEffect(() => { async function loadProtests() { setProtestsLoading(true); try { const vm = await loadLeagueProtests(league.id); setProtestsViewModel(vm); } catch (err) { console.error('Failed to load protests:', err); } finally { setProtestsLoading(false); } } if (activeTab === 'protests') { loadProtests(); } }, [league.id, activeTab]); const handleApproveRequest = async (requestId: string) => { try { const updated = await approveLeagueJoinRequest(league.id, requestId); setJoinRequests(updated); onLeagueUpdate?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to approve request'); } }; const handleRejectRequest = async (requestId: string, trimmedReason: string) => { try { // Alpha-only: we do not persist the reason yet, but we at least log it. console.log('Join request rejected with reason:', { requestId, reason: trimmedReason, }); const updated = await rejectLeagueJoinRequest(league.id, requestId); setJoinRequests(updated); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to reject request'); } }; const handleRemoveMember = async (driverId: string) => { if (!confirm('Are you sure you want to remove this member?')) { return; } try { await removeLeagueMemberCommand(league.id, currentDriverId, driverId); onLeagueUpdate?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to remove member'); } }; const handleUpdateRole = async (driverId: string, newRole: MembershipRole) => { try { await updateLeagueMemberRoleCommand(league.id, currentDriverId, driverId, newRole); onLeagueUpdate?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update role'); } }; const modal = searchParams?.get('modal'); const modalRequestId = searchParams?.get('requestId'); const activeRejectRequest = modal === 'reject-request' ? joinRequests.find((r) => r.id === modalRequestId) ?? null : null; useEffect(() => { if (!activeRejectRequest) { setRejectReason(''); } }, [activeRejectRequest, setRejectReason]); const isRejectModalOpen = modal === 'reject-request' && !!activeRejectRequest; const openRejectModal = (requestId: string) => { const params = new URLSearchParams(searchParams?.toString()); params.set('modal', 'reject-request'); params.set('requestId', requestId); const query = params.toString(); const url = query ? `${pathname}?${query}` : pathname; router.push(url, { scroll: false }); }; const closeModal = () => { const params = new URLSearchParams(searchParams?.toString()); params.delete('modal'); params.delete('requestId'); const query = params.toString(); const url = query ? `${pathname}?${query}` : pathname; router.push(url, { scroll: false }); }; return (
{error && (
{error}
)} {/* Admin Tabs */}
{/* Tab Content */} {activeTab === 'members' && (

Manage Members

)} {activeTab === 'requests' && (

Join Requests

{loading ? (
Loading requests...
) : joinRequests.length === 0 ? (
No pending join requests
) : (
{joinRequests.map((request) => { const driver = request.driver; const requestedOn = new Date(request.requestedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); const metaPieces = [ `Requested ${requestedOn}`, request.message ? `Message: "${request.message}"` : null, ].filter(Boolean); const meta = metaPieces.join(' • '); return (
{driver ? ( ) : (

Unknown Driver

Unable to load driver details

)}
); })}
)}
)} {activeTab === 'races' && (

Schedule Race

Create a new race for this league; this is an alpha-only in-memory scheduler.

{ router.push(`/races/${race.id}`); }} />
)} {activeTab === 'protests' && (

Protests

Review protests filed by drivers and manage steward decisions

Alpha Preview
{protestsLoading ? (
Loading protests...
) : !protestsViewModel || protestsViewModel.protests.length === 0 ? (

No Protests Filed

When drivers file protests for incidents during races, they will appear here for steward review.

) : (
{/* Stats summary */}
Pending
{protestsViewModel.protests.filter((p) => p.status === 'pending').length}
Resolved
{protestsViewModel.protests.filter((p) => p.status === 'upheld' || p.status === 'dismissed').length}
Total
{protestsViewModel.protests.length}
{/* Protest list */}
{protestsViewModel.protests.map((protest) => { const race = protestsViewModel.racesById[protest.raceId]; const filer = protestsViewModel.driversById[protest.protestingDriverId]; const accused = protestsViewModel.driversById[protest.accusedDriverId]; const statusConfig = { pending: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', border: 'border-warning-amber/30', icon: Clock, label: 'Pending Review' }, under_review: { color: 'text-primary-blue', bg: 'bg-primary-blue/10', border: 'border-primary-blue/30', icon: Flag, label: 'Under Review' }, awaiting_defense: { color: 'text-purple-400', bg: 'bg-purple-500/10', border: 'border-purple-500/30', icon: Clock, label: 'Awaiting Defense' }, upheld: { color: 'text-red-400', bg: 'bg-red-500/10', border: 'border-red-500/30', icon: AlertTriangle, label: 'Upheld' }, dismissed: { color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-gray-500/30', icon: XCircle, label: 'Dismissed' }, withdrawn: { color: 'text-gray-500', bg: 'bg-gray-600/10', border: 'border-gray-600/30', icon: XCircle, label: 'Withdrawn' }, }[protest.status] ?? { color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-gray-500/30', icon: Clock, label: protest.status }; const StatusIcon = statusConfig.icon; return (
{statusConfig.label}
{race && ( {race.track} • {new Date(race.scheduledAt).toLocaleDateString()} )}

Incident at Lap {protest.incident.lap}

{protest.incident.description}

Filed by: {filer?.name ?? 'Unknown'}
Against: {accused?.name ?? 'Unknown'}
{protest.status === 'pending' && (
)}
{protest.comment && (
Driver comment: "{protest.comment}"
)}
); })}

Alpha Note: Protest review and penalty application is demonstration-only. In the full product, stewards can review evidence, apply penalties, and manage appeals.

)}
)} {activeTab === 'sponsorships' && ( )} {activeTab === 'fees' && ( )} {activeTab === 'wallet' && (

League Wallet

Track revenue from sponsorships and membership fees

Alpha Preview
Balance
$0.00
Total Revenue
$0.00
Platform Fees
$0.00

No Transactions

Revenue from sponsorships and membership fees will appear here.

Withdrawal Note: Funds can only be withdrawn after the season is completed. A 10% platform fee applies to all revenue.

)} {activeTab === 'prizes' && (

Season Prizes

Define prizes for championship positions

No Prizes Defined

Add prizes to be awarded to drivers at the end of the season based on final standings.

Alpha Note: Prize management is demonstration-only. In production, prizes are paid from the league wallet after season completion.

)} {activeTab === 'liveries' && (

Livery Management

Upload templates and download composited livery packs

Alpha Preview
{/* Livery Templates Section */}

Livery Templates

Upload base liveries for each car allowed in the league. Position sponsor decals on these templates.

{/* Example car templates */} {[ { id: 'car-1', name: 'Porsche 911 GT3 R', hasTemplate: false }, { id: 'car-2', name: 'Ferrari 488 GT3', hasTemplate: false }, ].map((car) => (

{car.name}

{car.hasTemplate ? 'Template uploaded' : 'No template'}

))}
{/* Download Livery Pack Section */}

Download Livery Pack

Generate a .zip file containing all driver liveries with sponsor decals burned in. Members and admins can use this pack in-game.

Drivers
0
with uploaded liveries
Templates
0
cars configured
Sponsors
0
active this season

Estimated size: ~50MB • Includes all drivers

{/* Decal Placement Info */}

How It Works

1. Template Setup

Upload base liveries for each car. Position where sponsor logos will appear.

2. Driver Liveries

Drivers upload their personal liveries. Must be clean (no logos/text).

3. Sponsor Decals

Sponsor logos are automatically placed based on your template positions.

4. Pack Generation

Download .zip with all liveries composited. Ready for in-game use.

Alpha Note: Livery compositing and pack generation are demonstration-only. In production, the system automatically validates liveries, places sponsor decals, and generates downloadable packs. The companion app will also auto-install packs.

)} {activeTab === 'settings' && (

League Settings

{configLoading && !configForm ? (
Loading configuration…
) : configForm ? (

Alpha Demo Season

At a glance

Structure:{' '} {configForm.structure.mode === 'solo' ? `Solo • ${configForm.structure.maxDrivers} drivers` : `Teams • ${configForm.structure.maxTeams ?? '—'} × ${ configForm.structure.driversPerTeam ?? '—' } drivers (${configForm.structure.maxDrivers ?? '—'} total)`}

Schedule:{' '} {`${configForm.timings.roundsPlanned ?? '?'} rounds • ${ configForm.timings.qualifyingMinutes } min Qualifying • ${configForm.timings.mainRaceMinutes} min Race`}

Scoring:{' '} {league.settings.pointsSystem.toUpperCase()}

League Owner

{ownerSummary ? ( ) : (

Loading owner details...

)}

League settings editing is alpha-only and changes are not persisted yet.

) : (
Unable to load league configuration for this demo league.
)}
)} { const trimmed = rejectReason.trim(); if (!trimmed) { setError('A rejection reason is required to reject a join request.'); return; } if (!activeRejectRequest) { return; } await handleRejectRequest(activeRejectRequest.id, trimmed); closeModal(); }} onSecondaryAction={() => { setRejectReason(''); }} onOpenChange={(open) => { if (!open) { closeModal(); } }} isOpen={isRejectModalOpen} >

This will remove the join request and the driver will not be added to the league.