website refactor

This commit is contained in:
2026-01-17 15:46:55 +01:00
parent 4d5ce9bfd6
commit 72a626ce71
346 changed files with 19308 additions and 8605 deletions

View File

@@ -1,5 +1,5 @@
import { notFound } from 'next/navigation';
import { LeagueDetailTemplate } from '@/templates/LeagueDetailTemplate';
import { LeagueOverviewTemplate } from '@/templates/LeagueOverviewTemplate';
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';
import { LeagueDetailViewDataBuilder } from '@/lib/builders/view-data/LeagueDetailViewDataBuilder';
import { ErrorBanner } from '@/ui/ErrorBanner';
@@ -49,8 +49,6 @@ export default async function Page({ params }: Props) {
});
return (
<LeagueDetailTemplate viewData={viewData} tabs={[]}>
{null}
</LeagueDetailTemplate>
<LeagueOverviewTemplate viewData={viewData} />
);
}

View File

@@ -16,13 +16,14 @@ import {
createRaceAction,
updateRaceAction,
deleteRaceAction
} from './actions';
} from '@/app/actions/leagueScheduleActions';
import { RaceScheduleCommandModel } from '@/lib/command-models/leagues/RaceScheduleCommandModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { ConfirmDialog } from '@/components/shared/ux/ConfirmDialog';
export function LeagueAdminSchedulePageClient() {
const params = useParams();
@@ -39,6 +40,8 @@ export function LeagueAdminSchedulePageClient() {
const [isPublishing, setIsPublishing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [deletingRaceId, setDeletingRaceId] = useState<string | null>(null);
const [raceToDelete, setRaceToDelete] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
// Check admin status using domain hook
const { data: isAdmin, isLoading: isAdminLoading } = useLeagueAdminStatus(leagueId, currentDriverId);
@@ -48,7 +51,7 @@ export function LeagueAdminSchedulePageClient() {
// Auto-select season
const selectedSeasonId = seasonId || (seasonsData && seasonsData.length > 0
? (seasonsData.find((s) => s.status === 'active') ?? seasonsData[0])?.seasonId
? (seasonsData.find((s) => s.status === 'active') ?? seasonsData[0])?.seasonId || ''
: '');
// Load schedule using domain hook
@@ -65,6 +68,7 @@ export function LeagueAdminSchedulePageClient() {
if (!schedule || !selectedSeasonId) return;
setIsPublishing(true);
setError(null);
try {
const result = schedule.published
? await unpublishScheduleAction(leagueId, selectedSeasonId)
@@ -73,7 +77,7 @@ export function LeagueAdminSchedulePageClient() {
if (result.isOk()) {
router.refresh();
} else {
alert(result.getError());
setError(result.getError());
}
} finally {
setIsPublishing(false);
@@ -89,6 +93,7 @@ export function LeagueAdminSchedulePageClient() {
}
setIsSaving(true);
setError(null);
try {
const result = !editingRaceId
? await createRaceAction(leagueId, selectedSeasonId, form.toCommand())
@@ -100,7 +105,7 @@ export function LeagueAdminSchedulePageClient() {
setEditingRaceId(null);
router.refresh();
} else {
alert(result.getError());
setError(result.getError());
}
} finally {
setIsSaving(false);
@@ -120,18 +125,22 @@ export function LeagueAdminSchedulePageClient() {
}));
};
const handleDelete = async (raceId: string) => {
if (!selectedSeasonId) return;
const confirmed = window.confirm('Delete this race?');
if (!confirmed) return;
const handleDelete = (raceId: string) => {
setRaceToDelete(raceId);
};
const confirmDelete = async () => {
if (!selectedSeasonId || !raceToDelete) return;
setDeletingRaceId(raceId);
setDeletingRaceId(raceToDelete);
setError(null);
try {
const result = await deleteRaceAction(leagueId, selectedSeasonId, raceId);
const result = await deleteRaceAction(leagueId, selectedSeasonId, raceToDelete);
if (result.isOk()) {
router.refresh();
setRaceToDelete(null);
} else {
alert(result.getError());
setError(result.getError());
}
} finally {
setDeletingRaceId(null);
@@ -186,34 +195,47 @@ export function LeagueAdminSchedulePageClient() {
if (!data) return null;
return (
<LeagueAdminScheduleTemplate
viewData={data}
onSeasonChange={handleSeasonChange}
onPublishToggle={handlePublishToggle}
onAddOrSave={handleAddOrSave}
onEdit={handleEdit}
onDelete={handleDelete}
onCancelEdit={handleCancelEdit}
track={form.track}
car={form.car}
scheduledAtIso={form.scheduledAtIso}
editingRaceId={editingRaceId}
isPublishing={isPublishing}
isSaving={isSaving}
isDeleting={deletingRaceId}
setTrack={(val) => {
form.track = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
setCar={(val) => {
form.car = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
setScheduledAtIso={(val) => {
form.scheduledAtIso = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
/>
<>
<LeagueAdminScheduleTemplate
viewData={data}
onSeasonChange={handleSeasonChange}
onPublishToggle={handlePublishToggle}
onAddOrSave={handleAddOrSave}
onEdit={handleEdit}
onDelete={handleDelete}
onCancelEdit={handleCancelEdit}
track={form.track}
car={form.car}
scheduledAtIso={form.scheduledAtIso}
editingRaceId={editingRaceId}
isPublishing={isPublishing}
isSaving={isSaving}
isDeleting={deletingRaceId}
error={error}
setTrack={(val) => {
form.track = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
setCar={(val) => {
form.car = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
setScheduledAtIso={(val) => {
form.scheduledAtIso = val;
setForm(new RaceScheduleCommandModel(form.toCommand()));
}}
/>
<ConfirmDialog
isOpen={!!raceToDelete}
onClose={() => setRaceToDelete(null)}
onConfirm={confirmDelete}
title="Delete Race"
description="Are you sure you want to delete this race? This will remove it from the schedule and cannot be undone."
confirmLabel="Delete Race"
variant="danger"
isLoading={!!deletingRaceId}
/>
</>
);
};

View File

@@ -1,70 +0,0 @@
'use server';
import { revalidatePath } from 'next/cache';
import { Result } from '@/lib/contracts/Result';
import { ScheduleAdminMutation } from '@/lib/mutations/leagues/ScheduleAdminMutation';
import { routes } from '@/lib/routing/RouteConfig';
export async function publishScheduleAction(leagueId: string, seasonId: string): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.publishSchedule(leagueId, seasonId);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function unpublishScheduleAction(leagueId: string, seasonId: string): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.unpublishSchedule(leagueId, seasonId);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function createRaceAction(
leagueId: string,
seasonId: string,
input: { track: string; car: string; scheduledAtIso: string }
): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.createRace(leagueId, seasonId, input);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function updateRaceAction(
leagueId: string,
seasonId: string,
raceId: string,
input: Partial<{ track: string; car: string; scheduledAtIso: string }>
): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.updateRace(leagueId, seasonId, raceId, input);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function deleteRaceAction(leagueId: string, seasonId: string, raceId: string): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.deleteRace(leagueId, seasonId, raceId);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}

View File

@@ -1,5 +1,6 @@
'use client';
import { StewardingQueuePanel } from '@/components/leagues/StewardingQueuePanel';
import { PenaltyFAB } from '@/ui/PenaltyFAB';
import { QuickPenaltyModal } from '@/components/leagues/QuickPenaltyModal';
import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
@@ -8,12 +9,10 @@ import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { useLeagueStewardingMutations } from "@/hooks/league/useLeagueStewardingMutations";
import { useMemo, useState } from 'react';
import { PendingProtestsList } from '@/components/leagues/PendingProtestsList';
import { PenaltyHistoryList } from '@/components/leagues/PenaltyHistoryList';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import type { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData';
import { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
import { RaceViewModel } from '@/lib/view-models/RaceViewModel';
@@ -26,7 +25,7 @@ interface StewardingTemplateProps {
onRefetch: () => void;
}
export function StewardingPageClient({ data, leagueId, currentDriverId, onRefetch }: StewardingTemplateProps) {
export function StewardingPageClient({ data, currentDriverId, onRefetch }: StewardingTemplateProps) {
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending');
const [selectedProtest, setSelectedProtest] = useState<ProtestViewModel | null>(null);
const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false);
@@ -36,19 +35,16 @@ export function StewardingPageClient({ data, leagueId, currentDriverId, onRefetc
// Flatten protests for the specialized list components
const allPendingProtests = useMemo(() => {
return data.races.flatMap(r => r.pendingProtests.map(p => new ProtestViewModel({
return data.races.flatMap(r => r.pendingProtests.map(p => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
raceName: r.track || 'Unknown Track',
protestingDriver: data.drivers.find(d => d.id === p.protestingDriverId)?.name || 'Unknown',
accusedDriver: data.drivers.find(d => d.id === p.accusedDriverId)?.name || 'Unknown',
description: p.incident.description,
submittedAt: p.filedAt,
status: p.status,
raceId: r.id,
incident: p.incident,
proofVideoUrl: p.proofVideoUrl,
decisionNotes: p.decisionNotes,
} as never)));
}, [data.races]);
status: p.status as 'pending' | 'under_review' | 'resolved' | 'rejected',
})));
}, [data.races, data.drivers]);
const allResolvedProtests = useMemo(() => {
return data.races.flatMap(r => r.resolvedProtests.map(p => new ProtestViewModel({
@@ -131,84 +127,91 @@ export function StewardingPageClient({ data, leagueId, currentDriverId, onRefetc
});
};
const handleReviewProtest = (id: string) => {
// Find the protest in the data
let foundProtest: ProtestViewModel | null = null;
data.races.forEach(r => {
const p = r.pendingProtests.find(p => p.id === id);
if (p) {
foundProtest = new ProtestViewModel({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
description: p.incident.description,
submittedAt: p.filedAt,
status: p.status,
raceId: r.id,
incident: p.incident,
proofVideoUrl: p.proofVideoUrl,
decisionNotes: p.decisionNotes,
} as never);
}
});
if (foundProtest) setSelectedProtest(foundProtest);
};
return (
<Stack gap={6}>
<Card>
<Box p={6}>
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
<Box>
<Heading level={2}>Stewarding</Heading>
<Box mt={1}>
<Text size="sm" color="text-gray-400">
Quick overview of protests and penalties across all races
</Text>
</Box>
</Box>
<StewardingStats
totalPending={data.totalPending}
totalResolved={data.totalResolved}
totalPenalties={data.totalPenalties}
/>
{/* Tab navigation */}
<Box borderBottom borderColor="border-charcoal-outline">
<Stack direction="row" gap={4}>
<Box
borderBottom={activeTab === 'pending'}
borderColor={activeTab === 'pending' ? 'border-primary-blue' : undefined}
>
<Button
variant="ghost"
onClick={() => setActiveTab('pending')}
rounded="none"
>
<Stack direction="row" align="center" gap={2}>
<Text weight="medium" color={activeTab === 'pending' ? 'text-primary-blue' : undefined}>Pending Protests</Text>
{data.totalPending > 0 && (
<Box px={2} py={0.5} fontSize="0.75rem" bg="bg-warning-amber/20" color="text-warning-amber" rounded="full">
{data.totalPending}
</Box>
)}
</Stack>
</Button>
</Box>
{/* Stats summary */}
<StewardingStats
totalPending={data.totalPending}
totalResolved={data.totalResolved}
totalPenalties={data.totalPenalties}
/>
{/* Tab navigation */}
<Box borderBottom borderColor="border-charcoal-outline" mb={6}>
<Stack direction="row" gap={4}>
<Box
borderBottom={activeTab === 'pending'}
borderColor={activeTab === 'pending' ? 'border-primary-blue' : undefined}
>
<Button
variant="ghost"
onClick={() => setActiveTab('pending')}
rounded="none"
>
<Stack direction="row" align="center" gap={2}>
<Text weight="medium" color={activeTab === 'pending' ? 'text-primary-blue' : undefined}>Pending Protests</Text>
{data.totalPending > 0 && (
<Box px={2} py={0.5} fontSize="0.75rem" bg="bg-warning-amber/20" color="text-warning-amber" rounded="full">
{data.totalPending}
</Box>
)}
</Stack>
</Button>
</Box>
<Box
borderBottom={activeTab === 'history'}
borderColor={activeTab === 'history' ? 'border-primary-blue' : undefined}
>
<Button
variant="ghost"
onClick={() => setActiveTab('history')}
rounded="none"
>
<Text weight="medium" color={activeTab === 'history' ? 'text-primary-blue' : undefined}>History</Text>
</Button>
</Box>
</Stack>
<Box
borderBottom={activeTab === 'history'}
borderColor={activeTab === 'history' ? 'border-primary-blue' : undefined}
>
<Button
variant="ghost"
onClick={() => setActiveTab('history')}
rounded="none"
>
<Text weight="medium" color={activeTab === 'history' ? 'text-primary-blue' : undefined}>History</Text>
</Button>
</Box>
</Stack>
</Box>
{/* Content */}
{activeTab === 'pending' ? (
<PendingProtestsList
protests={allPendingProtests}
races={racesMap}
drivers={driverMap}
leagueId={leagueId}
onReviewProtest={setSelectedProtest}
onProtestReviewed={onRefetch}
/>
) : (
{/* Content */}
{activeTab === 'pending' ? (
<StewardingQueuePanel
protests={allPendingProtests}
onReview={handleReviewProtest}
/>
) : (
<Card>
<Box p={6}>
<PenaltyHistoryList
protests={allResolvedProtests}
races={racesMap}
drivers={driverMap}
/>
)}
</Box>
</Card>
</Box>
</Card>
)}
{activeTab === 'history' && (
<PenaltyFAB onClick={() => setShowQuickPenaltyModal(true)} />

View File

@@ -1,26 +1,18 @@
'use client';
import React, { useState, useMemo } from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { TransactionRow } from '@/components/leagues/TransactionRow';
import React from 'react';
import { WalletSummaryPanel } from '@/components/leagues/WalletSummaryPanel';
import type { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid';
import { Icon as UIIcon } from '@/ui/Icon';
import {
Wallet,
DollarSign,
ArrowUpRight,
Clock,
AlertTriangle,
Download,
TrendingUp
Download
} from 'lucide-react';
import { Button } from '@/ui/Button';
interface WalletTemplateProps {
viewData: LeagueWalletViewData;
@@ -29,29 +21,15 @@ interface WalletTemplateProps {
mutationLoading?: boolean;
}
export function LeagueWalletPageClient({ viewData, onWithdraw, onExport, mutationLoading = false }: WalletTemplateProps) {
const [withdrawAmount, setWithdrawAmount] = useState('');
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
const [filterType, setFilterType] = useState<'all' | 'sponsorship' | 'membership' | 'withdrawal' | 'prize'>('all');
const filteredTransactions = useMemo(() => {
if (filterType === 'all') return viewData.transactions;
return viewData.transactions.filter(t => t.type === filterType);
}, [viewData.transactions, filterType]);
const handleWithdrawClick = () => {
const amount = parseFloat(withdrawAmount);
if (!amount || amount <= 0) return;
if (onWithdraw) {
onWithdraw(amount);
setShowWithdrawModal(false);
setWithdrawAmount('');
}
};
const canWithdraw = viewData.balance > 0;
const withdrawalBlockReason = !canWithdraw ? 'Balance is zero' : undefined;
export function LeagueWalletPageClient({ viewData, onExport }: WalletTemplateProps) {
// Map transactions to the format expected by WalletSummaryPanel
const transactions = viewData.transactions.map(t => ({
id: t.id,
type: t.type === 'withdrawal' ? 'debit' : 'credit' as 'credit' | 'debit',
amount: parseFloat(t.formattedAmount.replace(/[^0-9.-]+/g, '')),
description: t.description,
date: t.formattedDate,
}));
return (
<Container size="lg" py={8}>
@@ -61,314 +39,29 @@ export function LeagueWalletPageClient({ viewData, onWithdraw, onExport, mutatio
<Heading level={1}>League Wallet</Heading>
<Text color="text-gray-400">Manage your league&apos;s finances and payouts</Text>
</Box>
<Stack direction="row" align="center" gap={2}>
<Button variant="secondary" onClick={onExport}>
<Stack direction="row" align="center" gap={2}>
<UIIcon icon={Download} size={4} />
<Text>Export</Text>
</Stack>
</Button>
<Button
variant="primary"
onClick={() => setShowWithdrawModal(true)}
disabled={!canWithdraw || !onWithdraw}
>
<Stack direction="row" align="center" gap={2}>
<UIIcon icon={ArrowUpRight} size={4} />
<Text>Withdraw</Text>
</Stack>
</Button>
</Stack>
<Button variant="secondary" onClick={onExport}>
<Stack direction="row" align="center" gap={2}>
<UIIcon icon={Download} size={4} />
<Text>Export</Text>
</Stack>
</Button>
</Box>
{/* Withdrawal Warning */}
{!canWithdraw && withdrawalBlockReason && (
<Box mb={6} p={4} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30">
<Stack direction="row" align="start" gap={3}>
<UIIcon icon={AlertTriangle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<Box>
<Text weight="medium" color="text-warning-amber" block>Withdrawals Temporarily Unavailable</Text>
<Text size="sm" color="text-gray-400" block mt={1}>{withdrawalBlockReason}</Text>
</Box>
</Stack>
</Box>
)}
{/* Stats Grid */}
<Grid cols={1} mdCols={2} lgCols={4} gap={4} mb={8}>
<Card>
<Box p={4}>
<Stack direction="row" align="center" gap={3}>
<Box display="flex" h={12} w={12} alignItems="center" justifyContent="center" rounded="xl" bg="bg-performance-green/10">
<UIIcon icon={Wallet} size={6} color="text-performance-green" />
</Box>
<Box>
<Text size="2xl" weight="bold" color="text-white" block>{viewData.formattedBalance}</Text>
<Text size="sm" color="text-gray-400" block>Available Balance</Text>
</Box>
</Stack>
</Box>
</Card>
<Card>
<Box p={4}>
<Stack direction="row" align="center" gap={3}>
<Box display="flex" h={12} w={12} alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
<UIIcon icon={TrendingUp} size={6} color="text-primary-blue" />
</Box>
<Box>
<Text size="2xl" weight="bold" color="text-white" block>{viewData.formattedTotalRevenue}</Text>
<Text size="sm" color="text-gray-400" block>Total Revenue</Text>
</Box>
</Stack>
</Box>
</Card>
<Card>
<Box p={4}>
<Stack direction="row" align="center" gap={3}>
<Box display="flex" h={12} w={12} alignItems="center" justifyContent="center" rounded="xl" bg="bg-warning-amber/10">
<UIIcon icon={DollarSign} size={6} color="text-warning-amber" />
</Box>
<Box>
<Text size="2xl" weight="bold" color="text-white" block>{viewData.formattedTotalFees}</Text>
<Text size="sm" color="text-gray-400" block>Platform Fees (10%)</Text>
</Box>
</Stack>
</Box>
</Card>
<Card>
<Box p={4}>
<Stack direction="row" align="center" gap={3}>
<Box display="flex" h={12} w={12} alignItems="center" justifyContent="center" rounded="xl" bg="bg-purple-500/10">
<UIIcon icon={Clock} size={6} color="text-purple-400" />
</Box>
<Box>
<Text size="2xl" weight="bold" color="text-white" block>{viewData.formattedPendingPayouts}</Text>
<Text size="sm" color="text-gray-400" block>Pending Payouts</Text>
</Box>
</Stack>
</Box>
</Card>
</Grid>
{/* Transactions */}
<Card>
<Box display="flex" alignItems="center" justifyContent="between" p={4} borderBottom borderColor="border-charcoal-outline">
<Heading level={2}>Transaction History</Heading>
<Box as="select"
value={filterType}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFilterType(e.target.value as typeof filterType)}
p={1.5}
rounded="lg"
border
borderColor="border-charcoal-outline"
bg="bg-iron-gray"
color="text-white"
fontSize="sm"
>
<Box as="option" value="all">All Transactions</Box>
<Box as="option" value="sponsorship">Sponsorships</Box>
<Box as="option" value="membership">Memberships</Box>
<Box as="option" value="withdrawal">Withdrawals</Box>
<Box as="option" value="prize">Prizes</Box>
</Box>
</Box>
{filteredTransactions.length === 0 ? (
<Box py={12} textAlign="center">
<Box display="flex" justifyContent="center" mb={4}>
<UIIcon icon={Wallet} size={12} color="text-gray-500" />
</Box>
<Heading level={3}>No Transactions</Heading>
<Box mt={2}>
<Text color="text-gray-400">
{filterType === 'all'
? 'Revenue from sponsorships and fees will appear here.'
: `No ${filterType} transactions found.`}
</Text>
</Box>
</Box>
) : (
<Box>
{filteredTransactions.map((transaction) => (
<TransactionRow
key={transaction.id}
transaction={{
id: transaction.id,
type: transaction.type,
description: transaction.description,
formattedDate: transaction.formattedDate,
formattedAmount: transaction.formattedAmount,
typeColor: transaction.type === 'withdrawal' ? 'text-red-400' : 'text-performance-green',
status: transaction.status,
statusColor: transaction.status === 'completed' ? 'text-performance-green' : 'text-warning-amber',
amountColor: transaction.type === 'withdrawal' ? 'text-red-400' : 'text-performance-green',
}}
/>
))}
</Box>
)}
</Card>
{/* Revenue Breakdown */}
<Grid cols={1} lgCols={2} gap={6} mt={6}>
<Card>
<Box p={4}>
<Heading level={3} mb={4}>Revenue Breakdown</Heading>
<Stack gap={3}>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" gap={2}>
<Box w={3} h={3} rounded="full" bg="bg-primary-blue" />
<Text color="text-gray-400">Sponsorships</Text>
</Stack>
<Text weight="medium" color="text-white">$1,600.00</Text>
</Box>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" gap={2}>
<Box w={3} h={3} rounded="full" bg="bg-performance-green" />
<Text color="text-gray-400">Membership Fees</Text>
</Stack>
<Text weight="medium" color="text-white">$1,600.00</Text>
</Box>
<Box display="flex" alignItems="center" justifyContent="between" pt={2} borderTop borderColor="border-charcoal-outline">
<Text weight="medium" color="text-gray-300">Total Gross Revenue</Text>
<Text weight="bold" color="text-white">$3,200.00</Text>
</Box>
<Box display="flex" alignItems="center" justifyContent="between">
<Text size="sm" color="text-warning-amber">Platform Fee (10%)</Text>
<Text size="sm" color="text-warning-amber">-$320.00</Text>
</Box>
<Box display="flex" alignItems="center" justifyContent="between" pt={2} borderTop borderColor="border-charcoal-outline">
<Text weight="medium" color="text-performance-green">Net Revenue</Text>
<Text weight="bold" color="text-performance-green">$2,880.00</Text>
</Box>
</Stack>
</Box>
</Card>
<Card>
<Box p={4}>
<Heading level={3} mb={4}>Payout Schedule</Heading>
<Stack gap={3}>
<Box p={3} rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline">
<Box display="flex" alignItems="center" justifyContent="between" mb={1}>
<Text size="sm" weight="medium" color="text-white">Season 2 Prize Pool</Text>
<Text size="sm" weight="medium" color="text-warning-amber">Pending</Text>
</Box>
<Text size="xs" color="text-gray-500">
Distributed after season completion to top 3 drivers
</Text>
</Box>
<Box p={3} rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline">
<Box display="flex" alignItems="center" justifyContent="between" mb={1}>
<Text size="sm" weight="medium" color="text-white">Available for Withdrawal</Text>
<Text size="sm" weight="medium" color="text-performance-green">{viewData.formattedBalance}</Text>
</Box>
<Text size="xs" color="text-gray-500">
Available after Season 2 ends (estimated: Jan 15, 2026)
</Text>
</Box>
</Stack>
</Box>
</Card>
</Grid>
{/* Withdraw Modal */}
{showWithdrawModal && onWithdraw && (
<Box position="fixed" inset="0" bg="bg-black/50" display="flex" alignItems="center" justifyContent="center" zIndex={50}>
<Card>
<Box p={6} w="full" maxWidth="28rem">
<Heading level={2} mb={4}>Withdraw Funds</Heading>
{!canWithdraw ? (
<Box p={4} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" mb={4}>
<Text size="sm" color="text-warning-amber">{withdrawalBlockReason}</Text>
</Box>
) : (
<Stack gap={4}>
<Box>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
Amount to Withdraw
</Text>
<Box position="relative">
<Box position="absolute" left={3} top="50%" transform="translateY(-50%)">
<Text color="text-gray-500">$</Text>
</Box>
<Box as="input"
type="number"
value={withdrawAmount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setWithdrawAmount(e.target.value)}
max={viewData.balance}
w="full"
pl={8}
pr={4}
py={2}
rounded="lg"
border
borderColor="border-charcoal-outline"
bg="bg-iron-gray"
color="text-white"
placeholder="0.00"
/>
</Box>
<Text size="xs" color="text-gray-500" block mt={1}>
Available: {viewData.formattedBalance}
</Text>
</Box>
<Box>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
Destination
</Text>
<Box as="select"
w="full"
px={3}
py={2}
rounded="lg"
border
borderColor="border-charcoal-outline"
bg="bg-iron-gray"
color="text-white"
>
<Box as="option">Bank Account ***1234</Box>
</Box>
</Box>
</Stack>
)}
<Stack direction="row" gap={3} mt={6}>
<Button
variant="secondary"
onClick={() => setShowWithdrawModal(false)}
fullWidth
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleWithdrawClick}
disabled={!canWithdraw || mutationLoading || !withdrawAmount}
fullWidth
>
{mutationLoading ? 'Processing...' : 'Withdraw'}
</Button>
</Stack>
</Box>
</Card>
</Box>
)}
<WalletSummaryPanel
balance={viewData.balance}
currency="USD"
transactions={transactions}
onDeposit={() => {}} // Not implemented for leagues yet
onWithdraw={() => {}} // Not implemented for leagues yet
/>
{/* Alpha Notice */}
<Box mt={6} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
<Text size="xs" color="text-gray-400">
<Text weight="bold" color="text-warning-amber">Alpha Note:</Text> Wallet management is demonstration-only.
Real payment processing and bank integrations will be available when the payment system is fully implemented.
The 10% platform fee and season-based withdrawal restrictions are enforced in the actual implementation.
</Text>
</Box>
</Container>
);
}