website refactor
This commit is contained in:
@@ -2,13 +2,12 @@
|
||||
|
||||
import { CheckCircle2, Clock, Star } from 'lucide-react';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface AvailableLeague {
|
||||
@@ -53,33 +52,33 @@ export function AvailableLeagueCard({ league }: AvailableLeagueCardProps) {
|
||||
<Stack gap={4}>
|
||||
{/* Header */}
|
||||
<Stack direction="row" align="start" justify="between">
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Stack direction="row" align="center" gap={2} mb={1} wrap>
|
||||
<Badge variant="primary">{config.icon} {config.label}</Badge>
|
||||
<Box px={2} py={0.5} rounded="full" className={status.bgColor}>
|
||||
<Stack px={2} py={0.5} rounded="full" className={status.bgColor}>
|
||||
<Text size="xs" weight="medium" className={status.color}>{status.label}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Heading level={3}>{league.name}</Heading>
|
||||
<Text size="sm" color="text-gray-500" block mt={1}>{league.game}</Text>
|
||||
</Box>
|
||||
<Box px={2} py={1} rounded="lg" bg="bg-iron-gray/50">
|
||||
</Stack>
|
||||
<Stack px={2} py={1} rounded="lg" bg="bg-iron-gray/50">
|
||||
<Stack direction="row" align="center" gap={1}>
|
||||
<Icon icon={Star} size={3.5} color="text-yellow-400" />
|
||||
<Text size="sm" weight="medium" color="text-white">{league.rating}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Description */}
|
||||
<Text size="sm" color="text-gray-400" block truncate>{league.description}</Text>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<Box display="grid" gridCols={3} gap={2}>
|
||||
<Stack display="grid" gridCols={3} gap={2}>
|
||||
<StatItem label="Drivers" value={league.drivers} />
|
||||
<StatItem label="Avg Views" value={league.formattedAvgViews} />
|
||||
<StatItem label="CPM" value={league.formattedCpm} color="text-performance-green" />
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Next Race */}
|
||||
{league.nextRace && (
|
||||
@@ -106,21 +105,21 @@ export function AvailableLeagueCard({ league }: AvailableLeagueCardProps) {
|
||||
|
||||
{/* Actions */}
|
||||
<Stack direction="row" gap={2}>
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Link href={`/sponsor/leagues/${league.id}`} block>
|
||||
<Button variant="secondary" fullWidth size="sm">
|
||||
View Details
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
</Stack>
|
||||
{(league.mainSponsorSlot.available || league.secondarySlots.available > 0) && (
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Link href={`/sponsor/leagues/${league.id}?action=sponsor`} block>
|
||||
<Button variant="primary" fullWidth size="sm">
|
||||
Sponsor
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -130,22 +129,22 @@ export function AvailableLeagueCard({ league }: AvailableLeagueCardProps) {
|
||||
|
||||
function StatItem({ label, value, color = 'text-white' }: { label: string, value: string | number, color?: string }) {
|
||||
return (
|
||||
<Box p={2} bg="bg-iron-gray/50" rounded="lg" textAlign="center">
|
||||
<Stack p={2} bg="bg-iron-gray/50" rounded="lg" textAlign="center">
|
||||
<Text weight="bold" className={color}>{value}</Text>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>{label}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function SlotRow({ label, available, price }: { label: string, available: boolean, price: string }) {
|
||||
return (
|
||||
<Box p={2} rounded="lg" bg="bg-iron-gray/30">
|
||||
<Stack p={2} rounded="lg" bg="bg-iron-gray/30">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Box width="2.5" height="2.5" rounded="full" bg={available ? 'bg-performance-green' : 'bg-error-red'} />
|
||||
<Stack width="2.5" height="2.5" rounded="full" bg={available ? 'bg-performance-green' : 'bg-error-red'} />
|
||||
<Text size="sm" color="text-gray-300">{label}</Text>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack>
|
||||
{available ? (
|
||||
<Text size="sm" weight="semibold" color="text-white">{price}</Text>
|
||||
) : (
|
||||
@@ -154,8 +153,8 @@ function SlotRow({ label, available, price }: { label: string, available: boolea
|
||||
<Text size="sm" color="text-gray-500">Filled</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { InfoBanner } from '@/ui/InfoBanner';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { ModalIcon } from '@/ui/ModalIcon';
|
||||
|
||||
interface EndRaceModalProps {
|
||||
@@ -58,14 +57,14 @@ export function EndRaceModal({ raceId, raceName, onConfirm, onCancel, isOpen }:
|
||||
</Stack>
|
||||
</InfoBanner>
|
||||
|
||||
<Box textAlign="center">
|
||||
<Stack textAlign="center">
|
||||
<Text size="sm" color="text-gray-400">
|
||||
Race: <Text color="text-white" weight="medium">{raceName}</Text>
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
ID: {raceId}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
interface JoinRequestItemProps {
|
||||
@@ -22,7 +21,7 @@ export function JoinRequestItem({
|
||||
isRejecting,
|
||||
}: JoinRequestItemProps) {
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
@@ -33,7 +32,7 @@ export function JoinRequestItem({
|
||||
borderColor="border-charcoal-outline"
|
||||
>
|
||||
<Stack direction="row" align="center" gap={4} flexGrow={1}>
|
||||
<Box
|
||||
<Stack
|
||||
width="12"
|
||||
height="12"
|
||||
rounded="full"
|
||||
@@ -45,13 +44,13 @@ export function JoinRequestItem({
|
||||
style={{ fontSize: '1.125rem' }}
|
||||
>
|
||||
{driverId.charAt(0)}
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
</Stack>
|
||||
<Stack flexGrow={1}>
|
||||
<Text color="text-white" weight="medium" block>{driverId}</Text>
|
||||
<Text size="sm" color="text-gray-400" block>
|
||||
Requested {new Date(requestedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" gap={2}>
|
||||
<Button
|
||||
@@ -71,6 +70,6 @@ export function JoinRequestItem({
|
||||
{isRejecting ? 'Rejecting...' : 'Reject'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -22,36 +21,36 @@ interface JoinRequestsPanelProps {
|
||||
export function JoinRequestsPanel({ requests, onAccept, onDecline }: JoinRequestsPanelProps) {
|
||||
if (requests.length === 0) {
|
||||
return (
|
||||
<Box p={8} border borderDash borderColor="border-steel-grey" bg="surface-charcoal/20" textAlign="center">
|
||||
<Stack p={8} border borderDash borderColor="border-steel-grey" bg="surface-charcoal/20" textAlign="center">
|
||||
<Text color="text-gray-500" size="sm">No pending join requests</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
|
||||
<Box p={4} borderBottom borderColor="border-steel-grey" bg="base-graphite/50">
|
||||
<Stack border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
|
||||
<Stack p={4} borderBottom borderColor="border-steel-grey" bg="base-graphite/50">
|
||||
<Heading level={4} weight="bold" className="uppercase tracking-widest text-gray-400 text-[10px]">
|
||||
Pending Requests ({requests.length})
|
||||
</Heading>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack gap={0} className="divide-y divide-border-steel-grey/30">
|
||||
{requests.map((request) => (
|
||||
<Box key={request.id} p={4} className="hover:bg-white/[0.02] transition-colors">
|
||||
<Stack key={request.id} p={4} className="hover:bg-white/[0.02] transition-colors">
|
||||
<Stack direction="row" align="start" justify="between" gap={4}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box w="10" h="10" bg="base-graphite" border borderColor="border-steel-grey" display="flex" center>
|
||||
<Stack w="10" h="10" bg="base-graphite" border borderColor="border-steel-grey" display="flex" center>
|
||||
<Text size="xs" weight="bold" color="text-primary-blue">
|
||||
{request.driverName.substring(0, 2).toUpperCase()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text weight="bold" size="sm" color="text-white" block>{request.driverName}</Text>
|
||||
<Stack direction="row" align="center" gap={1.5} mt={0.5}>
|
||||
<Icon icon={Clock} size={3} color="text-gray-500" />
|
||||
<Text size="xs" color="text-gray-500" font="mono">{request.requestedAt}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
@@ -74,15 +73,15 @@ export function JoinRequestsPanel({ requests, onAccept, onDecline }: JoinRequest
|
||||
</Stack>
|
||||
</Stack>
|
||||
{request.message && (
|
||||
<Box mt={3} p={3} bg="base-graphite/30" borderLeft borderPrimary borderColor="primary-blue/40">
|
||||
<Stack mt={3} p={3} bg="base-graphite/30" borderLeft borderPrimary borderColor="primary-blue/40">
|
||||
<Text size="xs" color="text-gray-400" italic leading="relaxed">
|
||||
“{request.message}”
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import React from 'react';
|
||||
import { FileText, Gamepad2, Check } from 'lucide-react';
|
||||
import { Input } from '@/ui/Input';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -47,18 +46,18 @@ export function LeagueBasicsSection({
|
||||
return (
|
||||
<Stack gap={8}>
|
||||
{/* Emotional header for the step */}
|
||||
<Box textAlign="center" pb={2}>
|
||||
<Box mb={2}>
|
||||
<Stack textAlign="center" pb={2}>
|
||||
<Stack mb={2}>
|
||||
<Heading level={3}>
|
||||
Every great championship starts with a name
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box maxWidth="lg" mx="auto">
|
||||
</Stack>
|
||||
<Stack maxWidth="lg" mx="auto">
|
||||
<Text size="sm" color="text-gray-400">
|
||||
This is where legends begin. Give your league an identity that drivers will remember.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* League name */}
|
||||
<Stack gap={3}>
|
||||
@@ -130,11 +129,11 @@ export function LeagueBasicsSection({
|
||||
/>
|
||||
|
||||
<Surface variant="muted" rounded="lg" border padding={4}>
|
||||
<Box mb={3}>
|
||||
<Stack mb={3}>
|
||||
<Text size="xs" color="text-gray-400">
|
||||
<Text weight="medium" color="text-gray-300">Great descriptions include:</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Grid cols={3} gap={3}>
|
||||
{[
|
||||
'Racing style & pace',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { PlaceholderImage } from '@/ui/PlaceholderImage';
|
||||
import { Calendar as LucideCalendar, ChevronRight as LucideChevronRight } from 'lucide-react';
|
||||
@@ -50,7 +49,7 @@ export function LeagueCard({
|
||||
onClick,
|
||||
}: LeagueCardProps) {
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
position="relative"
|
||||
cursor={onClick ? 'pointer' : 'default'}
|
||||
h="full"
|
||||
@@ -58,7 +57,7 @@ export function LeagueCard({
|
||||
className="group"
|
||||
>
|
||||
{/* Card Container */}
|
||||
<Box
|
||||
<Stack
|
||||
position="relative"
|
||||
h="full"
|
||||
rounded="none"
|
||||
@@ -70,7 +69,7 @@ export function LeagueCard({
|
||||
className="hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300"
|
||||
>
|
||||
{/* Cover Image */}
|
||||
<Box position="relative" h="32" overflow="hidden">
|
||||
<Stack position="relative" h="32" overflow="hidden">
|
||||
<Image
|
||||
src={coverUrl}
|
||||
alt={`${name} cover`}
|
||||
@@ -80,21 +79,21 @@ export function LeagueCard({
|
||||
className="transition-transform duration-500 group-hover:scale-105 opacity-60"
|
||||
/>
|
||||
{/* Gradient Overlay */}
|
||||
<Box position="absolute" inset="0" bg="linear-gradient(to top, #0C0D0F, transparent)" />
|
||||
<Stack position="absolute" inset="0" bg="linear-gradient(to top, #0C0D0F, transparent)" />
|
||||
|
||||
{/* Badges - Top Left */}
|
||||
<Box position="absolute" top="3" left="3" display="flex" alignItems="center" gap={2}>
|
||||
<Stack position="absolute" top="3" left="3" display="flex" alignItems="center" gap={2}>
|
||||
{badges}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Championship Type Badge - Top Right */}
|
||||
<Box position="absolute" top="3" right="3">
|
||||
<Stack position="absolute" top="3" right="3">
|
||||
{championshipBadge}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Logo */}
|
||||
<Box position="absolute" left="4" bottom="-6" zIndex={10}>
|
||||
<Box w="12" h="12" rounded="none" overflow="hidden" border borderColor="border-gray/50" bg="graphite-black" shadow="xl">
|
||||
<Stack position="absolute" left="4" bottom="-6" zIndex={10}>
|
||||
<Stack w="12" h="12" rounded="none" overflow="hidden" border borderColor="border-gray/50" bg="graphite-black" shadow="xl">
|
||||
{logoUrl ? (
|
||||
<Image
|
||||
src={logoUrl}
|
||||
@@ -108,15 +107,15 @@ export function LeagueCard({
|
||||
) : (
|
||||
<PlaceholderImage size={48} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Content */}
|
||||
<Box pt={8} px={4} pb={4} display="flex" flexDirection="col" fullHeight>
|
||||
<Stack pt={8} px={4} pb={4} display="flex" flexDirection="col" fullHeight>
|
||||
{/* Title & Description */}
|
||||
<Stack direction="row" align="center" gap={2} mb={1}>
|
||||
<Box w="1" h="4" bg="primary-accent" />
|
||||
<Stack w="1" h="4" bg="primary-accent" />
|
||||
<Heading level={3} fontSize="lg" weight="bold" className="line-clamp-1 group-hover:text-primary-accent transition-colors tracking-tight">
|
||||
{name}
|
||||
</Heading>
|
||||
@@ -126,17 +125,17 @@ export function LeagueCard({
|
||||
</Text>
|
||||
|
||||
{/* Stats Row */}
|
||||
<Box display="flex" alignItems="center" gap={3} mb={4}>
|
||||
<Stack display="flex" alignItems="center" gap={3} mb={4}>
|
||||
{/* Primary Slots (Drivers/Teams/Nations) */}
|
||||
<Box flexGrow={1}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={1.5}>
|
||||
<Stack flexGrow={1}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" mb={1.5}>
|
||||
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">{slotLabel}</Text>
|
||||
<Text size="xs" color="text-gray-400" font="mono">
|
||||
{usedSlots}/{maxSlots || '∞'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box h="1" rounded="none" bg="border-gray/30" overflow="hidden">
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack h="1" rounded="none" bg="border-gray/30" overflow="hidden">
|
||||
<Stack
|
||||
h="full"
|
||||
rounded="none"
|
||||
transition
|
||||
@@ -149,45 +148,45 @@ export function LeagueCard({
|
||||
}
|
||||
style={{ width: `${Math.min(fillPercentage, 100)}%` }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Open Slots Badge */}
|
||||
{hasOpenSlots && (
|
||||
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="none" bg="primary-accent/5" border borderColor="primary-accent/20">
|
||||
<Box w="1.5" h="1.5" rounded="full" bg="primary-accent" className="animate-pulse" />
|
||||
<Stack display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="none" bg="primary-accent/5" border borderColor="primary-accent/20">
|
||||
<Stack w="1.5" h="1.5" rounded="full" bg="primary-accent" className="animate-pulse" />
|
||||
<Text size="xs" color="text-primary-accent" weight="bold" className="uppercase tracking-tighter">
|
||||
{openSlotsCount} OPEN
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Spacer to push footer to bottom */}
|
||||
<Box flexGrow={1} />
|
||||
<Stack flexGrow={1} />
|
||||
|
||||
{/* Footer Info */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-gray/30" mt="auto">
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-gray/30" mt="auto">
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
{timingSummary && (
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={LucideCalendar} size={3} color="text-gray-500" />
|
||||
<Text size="xs" color="text-gray-500" font="mono">
|
||||
{timingSummary.split('•')[1]?.trim() || timingSummary}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* View Arrow */}
|
||||
<Box display="flex" alignItems="center" gap={1} className="group-hover:text-primary-accent transition-colors">
|
||||
<Stack display="flex" alignItems="center" gap={1} className="group-hover:text-primary-accent transition-colors">
|
||||
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">VIEW</Text>
|
||||
<Icon icon={LucideChevronRight} size={3} color="text-gray-500" className="transition-transform group-hover:translate-x-0.5" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import {
|
||||
@@ -174,12 +173,12 @@ export function LeagueDecalPlacementEditor({
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack>
|
||||
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">{carName}</Heading>
|
||||
<Text size="sm" color="text-gray-400">Position sponsor decals on this car's template</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setZoom(z => Math.max(0.5, z - 0.25))}
|
||||
@@ -201,13 +200,13 @@ export function LeagueDecalPlacementEditor({
|
||||
>
|
||||
<Icon icon={ZoomIn} size={4} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, lg: 3 }} gap={6}>
|
||||
<Stack display="grid" responsiveGridCols={{ base: 1, lg: 3 }} gap={6}>
|
||||
{/* Canvas */}
|
||||
<Box responsiveColSpan={{ lg: 2 }}>
|
||||
<Box
|
||||
<Stack responsiveColSpan={{ lg: 2 }}>
|
||||
<Stack
|
||||
ref={canvasRef}
|
||||
position="relative"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -220,7 +219,7 @@ export function LeagueDecalPlacementEditor({
|
||||
>
|
||||
{/* Base Image or Placeholder */}
|
||||
{baseImageUrl ? (
|
||||
<Box
|
||||
<Stack
|
||||
as="img"
|
||||
src={baseImageUrl}
|
||||
alt="Livery template"
|
||||
@@ -231,21 +230,21 @@ export function LeagueDecalPlacementEditor({
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<Box fullWidth fullHeight display="flex" flexDirection="col" alignItems="center" justifyContent="center">
|
||||
<Stack fullWidth fullHeight display="flex" flexDirection="col" alignItems="center" justifyContent="center">
|
||||
<Icon icon={ImageIcon} size={16} color="text-gray-600"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="mb-2"
|
||||
/>
|
||||
<Text size="sm" color="text-gray-500">No base template uploaded</Text>
|
||||
<Text size="xs" color="text-gray-600">Upload a template image first</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Decal Placeholders */}
|
||||
{placements.map((placement) => {
|
||||
const decalColors = getSponsorTypeColor(placement.sponsorType);
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={placement.id}
|
||||
onMouseDown={(e: React.MouseEvent) => handleMouseDown(e, placement.id)}
|
||||
onClick={() => handleDecalClick(placement.id)}
|
||||
@@ -272,8 +271,8 @@ export function LeagueDecalPlacementEditor({
|
||||
transform: `translate(-50%, -50%) rotate(${placement.rotation}deg)`,
|
||||
}}
|
||||
>
|
||||
<Box textAlign="center" truncate px={1}>
|
||||
<Box
|
||||
<Stack textAlign="center" truncate px={1}>
|
||||
<Stack
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
transform="uppercase"
|
||||
@@ -281,36 +280,36 @@ export function LeagueDecalPlacementEditor({
|
||||
className="tracking-wide opacity-70"
|
||||
>
|
||||
{placement.sponsorType === 'main' ? 'Main' : 'Secondary'}
|
||||
</Box>
|
||||
<Box truncate>{placement.sponsorName}</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack truncate>{placement.sponsorName}</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Drag handle indicator */}
|
||||
{selectedDecal === placement.id && (
|
||||
<Box position="absolute" top="-1" left="-1" w="3" h="3" bg="bg-white" rounded="full" border borderWidth="2px" borderColor="border-primary-blue" />
|
||||
<Stack position="absolute" top="-1" left="-1" w="3" h="3" bg="bg-white" rounded="full" border borderWidth="2px" borderColor="border-primary-blue" />
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Grid overlay when dragging */}
|
||||
{isDragging && (
|
||||
<Box position="absolute" inset="0" pointerEvents="none">
|
||||
<Box fullWidth fullHeight
|
||||
<Stack position="absolute" inset="0" pointerEvents="none">
|
||||
<Stack fullWidth fullHeight
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(to right, rgba(255,255,255,0.05) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.05) 1px, transparent 1px)',
|
||||
backgroundSize: '10% 10%',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Text size="xs" color="text-gray-500" mt={2} block>
|
||||
Click a decal to select it, then drag to reposition. Use controls on the right to adjust size and rotation.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Controls Panel */}
|
||||
<Stack gap={4}>
|
||||
@@ -321,7 +320,7 @@ export function LeagueDecalPlacementEditor({
|
||||
{placements.map((placement) => {
|
||||
const decalColors = getSponsorTypeColor(placement.sponsorType);
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={placement.id}
|
||||
as="button"
|
||||
onClick={() => setSelectedDecal(placement.id)}
|
||||
@@ -335,9 +334,9 @@ export function LeagueDecalPlacementEditor({
|
||||
bg={selectedDecal === placement.id ? decalColors.bg : 'bg-iron-gray/30'}
|
||||
hoverBg={selectedDecal !== placement.id ? 'bg-iron-gray/50' : undefined}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box>
|
||||
<Box
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack>
|
||||
<Stack
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
weight="medium"
|
||||
@@ -345,19 +344,19 @@ export function LeagueDecalPlacementEditor({
|
||||
color={decalColors.text}
|
||||
>
|
||||
{placement.sponsorType === 'main' ? 'Main Sponsor' : `Secondary ${placement.sponsorType.split('-')[1]}`}
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
color="text-gray-500"
|
||||
mt={0.5}
|
||||
>
|
||||
{Math.round(placement.x * 100)}%, {Math.round(placement.y * 100)}% • {placement.rotation}°
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Icon icon={Target} size={4} color={selectedDecal === placement.id ? decalColors.text : 'text-gray-500'} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
@@ -369,12 +368,12 @@ export function LeagueDecalPlacementEditor({
|
||||
<Heading level={4} fontSize="sm" weight="semibold" color="text-white" mb={3}>Adjust Selected</Heading>
|
||||
|
||||
{/* Position */}
|
||||
<Box mb={4}>
|
||||
<Stack mb={4}>
|
||||
<Text as="label" size="xs" color="text-gray-400" block mb={2}>Position</Text>
|
||||
<Box display="grid" gridCols={2} gap={2}>
|
||||
<Box>
|
||||
<Stack display="grid" gridCols={2} gap={2}>
|
||||
<Stack>
|
||||
<Text as="label" size="xs" color="text-gray-500" block mb={1}>X</Text>
|
||||
<Box
|
||||
<Stack
|
||||
as="input"
|
||||
type="range"
|
||||
min="0"
|
||||
@@ -388,10 +387,10 @@ export function LeagueDecalPlacementEditor({
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="appearance-none cursor-pointer accent-primary-blue"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text as="label" size="xs" color="text-gray-500" block mb={1}>Y</Text>
|
||||
<Box
|
||||
<Stack
|
||||
as="input"
|
||||
type="range"
|
||||
min="0"
|
||||
@@ -405,14 +404,14 @@ export function LeagueDecalPlacementEditor({
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="appearance-none cursor-pointer accent-primary-blue"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Size */}
|
||||
<Box mb={4}>
|
||||
<Stack mb={4}>
|
||||
<Text as="label" size="xs" color="text-gray-400" block mb={2}>Size</Text>
|
||||
<Box display="flex" gap={2}>
|
||||
<Stack display="flex" gap={2}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleResize(selectedPlacement.id, 0.9)}
|
||||
@@ -435,14 +434,14 @@ export function LeagueDecalPlacementEditor({
|
||||
/>
|
||||
Larger
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Rotation */}
|
||||
<Box mb={4}>
|
||||
<Stack mb={4}>
|
||||
<Text as="label" size="xs" color="text-gray-400" block mb={2}>Rotation: {selectedPlacement.rotation}°</Text>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Box
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Stack
|
||||
as="input"
|
||||
type="range"
|
||||
min="0"
|
||||
@@ -464,8 +463,8 @@ export function LeagueDecalPlacementEditor({
|
||||
>
|
||||
<Icon icon={RotateCw} size={4} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -484,14 +483,14 @@ export function LeagueDecalPlacementEditor({
|
||||
</Button>
|
||||
|
||||
{/* Help Text */}
|
||||
<Box p={3} rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
|
||||
<Stack p={3} rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
<Text weight="bold" color="text-gray-400">Tip:</Text> Main sponsor gets the largest, most prominent placement.
|
||||
Secondary sponsors get smaller positions. These decals will be burned onto all driver liveries.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Check, HelpCircle, TrendingDown, X, Zap } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
@@ -83,7 +82,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
if (!isOpen) return null;
|
||||
|
||||
return createPortal(
|
||||
<Box
|
||||
<Stack
|
||||
ref={flyoutRef}
|
||||
position="fixed"
|
||||
zIndex={50}
|
||||
@@ -96,7 +95,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
|
||||
>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
@@ -112,7 +111,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
|
||||
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
|
||||
</Stack>
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -126,19 +125,19 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
hoverBg="bg-charcoal-outline"
|
||||
>
|
||||
<Icon icon={X} size={4} color="text-gray-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack p={4}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>,
|
||||
</Stack>
|
||||
</Stack>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.Ref<HTMLButtonElement> }) {
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
@@ -155,7 +154,7 @@ function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: Re
|
||||
hoverBg="bg-primary-blue/10"
|
||||
>
|
||||
<Icon icon={HelpCircle} size={3.5} />
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,13 +173,13 @@ function DropRulesMockup() {
|
||||
const wouldBe = results.reduce((sum, r) => sum + r.pts, 0);
|
||||
|
||||
return (
|
||||
<Box bg="bg-deep-graphite" rounded="lg" p={4}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline/50" opacity={0.5}>
|
||||
<Stack bg="bg-deep-graphite" rounded="lg" p={4}>
|
||||
<Stack display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline/50" opacity={0.5}>
|
||||
<Text size="xs" weight="semibold" color="text-white">Best 4 of 6 Results</Text>
|
||||
</Box>
|
||||
<Box display="flex" gap={1} mb={3}>
|
||||
</Stack>
|
||||
<Stack display="flex" gap={1} mb={3}>
|
||||
{results.map((r, i) => (
|
||||
<Box
|
||||
<Stack
|
||||
key={i}
|
||||
flexGrow={1}
|
||||
p={2}
|
||||
@@ -209,14 +208,14 @@ function DropRulesMockup() {
|
||||
>
|
||||
{r.pts}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between" alignItems="center">
|
||||
</Stack>
|
||||
<Stack display="flex" justifyContent="between" alignItems="center">
|
||||
<Text size="xs" color="text-gray-500">Total counted:</Text>
|
||||
<Text font="mono" weight="semibold" color="text-performance-green" size="xs">{total} pts</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between" alignItems="center" mt={1}>
|
||||
</Stack>
|
||||
<Stack display="flex" justifyContent="between" alignItems="center" mt={1}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -231,8 +230,8 @@ function DropRulesMockup() {
|
||||
>
|
||||
{wouldBe} pts
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -363,18 +362,18 @@ export function LeagueDropSection({
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
{/* Section header */}
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
|
||||
<Icon icon={TrendingDown} size={5} color="text-primary-blue" />
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
</Stack>
|
||||
<Stack flexGrow={1}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Heading level={3}>Drop Rules</Heading>
|
||||
<InfoButton buttonRef={dropInfoRef} onClick={() => setShowDropFlyout(true)} />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="xs" color="text-gray-500">Protect from bad races</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Drop Rules Flyout */}
|
||||
<InfoFlyout
|
||||
@@ -389,7 +388,7 @@ export function LeagueDropSection({
|
||||
This protects against mechanical failures, bad luck, or occasional poor performances.
|
||||
</Text>
|
||||
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="tracking-wide"
|
||||
@@ -401,7 +400,7 @@ export function LeagueDropSection({
|
||||
Visual Example
|
||||
</Text>
|
||||
<DropRulesMockup />
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={2}>
|
||||
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
|
||||
@@ -414,9 +413,9 @@ export function LeagueDropSection({
|
||||
Drop Strategies
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Text size="base">✓</Text>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="xs" weight="medium" color="text-white" block
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -429,11 +428,11 @@ export function LeagueDropSection({
|
||||
>
|
||||
Every race affects standings. Best for short seasons.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Text size="base">🏆</Text>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="xs" weight="medium" color="text-white" block
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -446,11 +445,11 @@ export function LeagueDropSection({
|
||||
>
|
||||
Only your top N races count. Extra races are optional.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Text size="base">🗑️</Text>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="xs" weight="medium" color="text-white" block
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -463,15 +462,15 @@ export function LeagueDropSection({
|
||||
>
|
||||
Exclude your N worst results. Forgives bad days.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20" p={3}>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
<Stack rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20" p={3}>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Zap} size={3.5} color="text-primary-blue" flexShrink={0} mt={0.5} />
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="xs" color="text-gray-400"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '11px' }}
|
||||
@@ -479,20 +478,20 @@ export function LeagueDropSection({
|
||||
<Text weight="medium" color="text-primary-blue">Pro tip:</Text> For an 8-round season,
|
||||
"Best 6" or "Drop 2" are popular choices.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</InfoFlyout>
|
||||
|
||||
{/* Strategy buttons + N stepper inline */}
|
||||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||||
<Stack display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||||
{DROP_OPTIONS.map((option) => {
|
||||
const isSelected = dropPolicy.strategy === option.value;
|
||||
const ruleInfo = DROP_RULE_INFO[option.value];
|
||||
return (
|
||||
<Box key={option.value} display="flex" alignItems="center" position="relative">
|
||||
<Box
|
||||
<Stack key={option.value} display="flex" alignItems="center" position="relative">
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
@@ -516,7 +515,7 @@ export function LeagueDropSection({
|
||||
style={{ borderRightWidth: 0, borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
|
||||
>
|
||||
{/* Radio indicator */}
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="4"
|
||||
w="4"
|
||||
@@ -530,16 +529,16 @@ export function LeagueDropSection({
|
||||
transition
|
||||
>
|
||||
{isSelected && <Icon icon={Check} size={2.5} color="text-white" />}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Text size="sm">{option.emoji}</Text>
|
||||
<Text size="sm" weight="medium" color={isSelected ? 'text-white' : 'text-gray-400'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Info button - separate from main button */}
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
ref={(el: HTMLButtonElement | null) => { dropRuleRefs.current[option.value] = el; }}
|
||||
type="button"
|
||||
@@ -569,7 +568,7 @@ export function LeagueDropSection({
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="hover:text-primary-blue transition-colors"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Drop Rule Info Flyout */}
|
||||
<InfoFlyout
|
||||
@@ -594,18 +593,18 @@ export function LeagueDropSection({
|
||||
</Text>
|
||||
<Stack gap={1.5}>
|
||||
{ruleInfo.details.map((detail, idx) => (
|
||||
<Box key={idx} display="flex" alignItems="start" gap={2}>
|
||||
<Stack key={idx} display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3} color="text-performance-green" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">{detail}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30" p={3}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30" p={3}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Text size="base">{option.emoji}</Text>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="xs" color="text-gray-400" block
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -613,20 +612,20 @@ export function LeagueDropSection({
|
||||
Example
|
||||
</Text>
|
||||
<Text size="xs" weight="medium" color="text-white" block>{ruleInfo.example}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</InfoFlyout>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* N Stepper - only show when needed */}
|
||||
{needsN && (
|
||||
<Box display="flex" alignItems="center" gap={1} ml={2}>
|
||||
<Stack display="flex" alignItems="center" gap={1} ml={2}>
|
||||
<Text size="xs" color="text-gray-500" mr={1}>N =</Text>
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={disabled || (dropPolicy.n ?? 1) <= 1}
|
||||
@@ -647,11 +646,11 @@ export function LeagueDropSection({
|
||||
opacity={disabled || (dropPolicy.n ?? 1) <= 1 ? 0.4 : 1}
|
||||
>
|
||||
−
|
||||
</Box>
|
||||
<Box display="flex" h="7" w="10" alignItems="center" justifyContent="center" rounded="md" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/50">
|
||||
</Stack>
|
||||
<Stack display="flex" h="7" w="10" alignItems="center" justifyContent="center" rounded="md" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/50">
|
||||
<Text size="sm" weight="semibold" color="text-white">{dropPolicy.n ?? 1}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
@@ -672,10 +671,10 @@ export function LeagueDropSection({
|
||||
opacity={disabled ? 0.4 : 1}
|
||||
>
|
||||
+
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Explanation text */}
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Image } from '@/ui/Image';
|
||||
|
||||
@@ -23,10 +22,10 @@ export function LeagueHeader({
|
||||
statusContent,
|
||||
}: LeagueHeaderProps) {
|
||||
return (
|
||||
<Box mb={8}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
|
||||
<Stack mb={8}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" mb={6}>
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Box h="16" w="16" rounded="xl" overflow="hidden" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)', backgroundColor: '#1a1d23' }} shadow="lg">
|
||||
<Stack h="16" w="16" rounded="xl" overflow="hidden" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)', backgroundColor: '#1a1d23' }} shadow="lg">
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt={`${name} logo`}
|
||||
@@ -36,9 +35,9 @@ export function LeagueHeader({
|
||||
fullHeight
|
||||
objectFit="cover"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" gap={3} mb={1}>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack display="flex" alignItems="center" gap={3} mb={1}>
|
||||
<Heading level={1}>
|
||||
{name}
|
||||
{sponsorContent && (
|
||||
@@ -48,15 +47,15 @@ export function LeagueHeader({
|
||||
)}
|
||||
</Heading>
|
||||
{statusContent}
|
||||
</Box>
|
||||
</Stack>
|
||||
{description && (
|
||||
<Text color="text-gray-400" size="sm" maxWidth="xl" block>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Trophy, Users, Timer, Activity, type LucideIcon } from 'lucide-react';
|
||||
import type { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Card } from '@/ui/Card';
|
||||
|
||||
interface LeagueHeaderPanelProps {
|
||||
viewData: LeagueDetailViewData;
|
||||
@@ -16,24 +15,24 @@ interface LeagueHeaderPanelProps {
|
||||
|
||||
export function LeagueHeaderPanel({ viewData }: LeagueHeaderPanelProps) {
|
||||
return (
|
||||
<Surface variant="dark" border rounded="lg" padding={6} position="relative" overflow="hidden">
|
||||
<Card variant="outline" p={6} position="relative" overflow="hidden" className="bg-graphite-black">
|
||||
{/* Background Accent */}
|
||||
<Box
|
||||
<Stack
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
w="300px"
|
||||
h="100%"
|
||||
h="full"
|
||||
bg="bg-gradient-to-l from-primary-blue/5 to-transparent"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
>{null}</Stack>
|
||||
|
||||
<Stack direction={{ base: 'col', md: 'row' }} justify="between" align="center" gap={6}>
|
||||
<Stack gap={2}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box p={2} bg="bg-primary-blue/10" rounded="md" border borderColor="border-primary-blue/20">
|
||||
<Stack p={2} bg="bg-primary-blue/10" rounded="md" border borderColor="border-primary-blue/20">
|
||||
<Icon icon={Trophy} size={6} color="text-primary-blue" />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Heading level={1} letterSpacing="tight">
|
||||
{viewData.name}
|
||||
</Heading>
|
||||
@@ -64,7 +63,7 @@ export function LeagueHeaderPanel({ viewData }: LeagueHeaderPanelProps) {
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Surface>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ function StatItem({ icon, label, value, color }: { icon: LucideIcon, label: stri
|
||||
<Stack gap={1}>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Icon icon={icon} size={3.5} color="text-gray-500" />
|
||||
<Text size="xs" color="text-gray-500" weight="medium" letterSpacing="wider" display="block">
|
||||
<Text size="xs" color="text-gray-500" weight="medium" letterSpacing="wider" block>
|
||||
{label.toUpperCase()}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
|
||||
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Card } from '@/ui/Card';
|
||||
|
||||
interface League {
|
||||
leagueId: string;
|
||||
@@ -21,40 +18,40 @@ interface LeagueListItemProps {
|
||||
|
||||
export function LeagueListItem({ league, isAdmin }: LeagueListItemProps) {
|
||||
return (
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="lg"
|
||||
border
|
||||
padding={4}
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderColor: '#262626' }}
|
||||
<Card
|
||||
variant="outline"
|
||||
p={4}
|
||||
className="bg-graphite-black border-[#262626]"
|
||||
>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text weight="medium" color="text-white" block>{league.name}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1} style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{league.description}
|
||||
</Text>
|
||||
{league.membershipRole && (
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
Your role:{' '}
|
||||
<Text color="text-gray-400" style={{ textTransform: 'capitalize' }}>{league.membershipRole}</Text>
|
||||
<Stack direction="row" align="center" justify="between" fullWidth>
|
||||
<Stack style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text weight="medium" color="text-white" block>{league.name}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1} style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{league.description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Stack direction="row" align="center" gap={2} style={{ marginLeft: '1rem' }}>
|
||||
<Link
|
||||
href={`/leagues/${league.leagueId}`}
|
||||
variant="ghost"
|
||||
>
|
||||
<Text size="sm" color="text-gray-300">View</Text>
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link href={`/leagues/${league.leagueId}?tab=admin`} variant="ghost">
|
||||
<Button variant="primary" size="sm">
|
||||
Manage
|
||||
</Button>
|
||||
{league.membershipRole && (
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
Your role:{' '}
|
||||
<Text color="text-gray-400" style={{ textTransform: 'capitalize' }}>{league.membershipRole}</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" gap={2} style={{ marginLeft: '1rem' }}>
|
||||
<Link
|
||||
href={`/leagues/${league.leagueId}`}
|
||||
variant="ghost"
|
||||
>
|
||||
<Text size="sm" color="text-gray-300">View</Text>
|
||||
</Link>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Link href={`/leagues/${league.leagueId}?tab=admin`} variant="ghost">
|
||||
<Button variant="primary" size="sm">
|
||||
Manage
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Surface>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { DollarSign, Calendar, User, TrendingUp } from 'lucide-react';
|
||||
@@ -76,13 +75,13 @@ export function LeagueMembershipFeesSection({
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack>
|
||||
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Membership Fees</Heading>
|
||||
<Text size="sm" color="text-gray-400" mt={1} block>
|
||||
Charge drivers for league participation
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{!feeConfig.enabled && !readOnly && (
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -91,18 +90,18 @@ export function LeagueMembershipFeesSection({
|
||||
Enable Fees
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{!feeConfig.enabled ? (
|
||||
<Box textAlign="center" py={12} rounded="lg" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30">
|
||||
<Box w="16" h="16" mx="auto" mb={4} rounded="full" bg="bg-iron-gray/50" display="flex" alignItems="center" justifyContent="center">
|
||||
<Stack textAlign="center" py={12} rounded="lg" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30">
|
||||
<Stack w="16" h="16" mx="auto" mb={4} rounded="full" bg="bg-iron-gray/50" display="flex" alignItems="center" justifyContent="center">
|
||||
<Icon icon={DollarSign} size={8} color="text-gray-500" />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Heading level={4} fontSize="lg" weight="medium" color="text-white" mb={2}>No Membership Fees</Heading>
|
||||
<Text size="sm" color="text-gray-400" maxWidth="md" mx="auto" block>
|
||||
This league is free to join. Enable membership fees to charge drivers for participation.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{/* Fee Type Selection */}
|
||||
@@ -110,13 +109,13 @@ export function LeagueMembershipFeesSection({
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
|
||||
Fee Type
|
||||
</Text>
|
||||
<Box display="grid" gridCols={3} gap={3}>
|
||||
<Stack display="grid" gridCols={3} gap={3}>
|
||||
{(['season', 'monthly', 'per_race'] as FeeType[]).map((type) => {
|
||||
const FeeIcon = type === 'season' ? Calendar : type === 'monthly' ? TrendingUp : User;
|
||||
const isSelected = feeConfig.type === type;
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={type}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -137,10 +136,10 @@ export function LeagueMembershipFeesSection({
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
{typeDescriptions[type]}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Amount Configuration */}
|
||||
@@ -150,8 +149,8 @@ export function LeagueMembershipFeesSection({
|
||||
</Text>
|
||||
|
||||
{editing ? (
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box flexGrow={1}>
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack flexGrow={1}>
|
||||
<Input
|
||||
type="number"
|
||||
value={tempAmount}
|
||||
@@ -160,7 +159,7 @@ export function LeagueMembershipFeesSection({
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
@@ -175,17 +174,17 @@ export function LeagueMembershipFeesSection({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<Box display="flex" alignItems="center" justifyContent="between" p={4} rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline">
|
||||
<Box>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" p={4} rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline">
|
||||
<Stack>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>
|
||||
${feeConfig.amount.toFixed(2)}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" mt={1} block>
|
||||
{typeLabels[feeConfig.type]}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -195,49 +194,49 @@ export function LeagueMembershipFeesSection({
|
||||
Edit Amount
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Revenue Breakdown */}
|
||||
{feeConfig.amount > 0 && (
|
||||
<Box display="grid" gridCols={2} gap={4}>
|
||||
<Box rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
|
||||
<Stack display="grid" gridCols={2} gap={4}>
|
||||
<Stack rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
|
||||
<Text size="xs" color="text-gray-400" block mb={1}>Platform Fee (10%)</Text>
|
||||
<Text size="lg" weight="bold" color="text-warning-amber" block>
|
||||
-${platformFee.toFixed(2)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
|
||||
</Stack>
|
||||
<Stack rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
|
||||
<Text size="xs" color="text-gray-400" block mb={1}>Net per Driver</Text>
|
||||
<Text size="lg" weight="bold" color="text-performance-green" block>
|
||||
${netAmount.toFixed(2)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Disable Fees */}
|
||||
{!readOnly && (
|
||||
<Box pt={4} borderTop borderColor="border-charcoal-outline">
|
||||
<Stack pt={4} borderTop borderColor="border-charcoal-outline">
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => setFeeConfig({ type: 'season', amount: 0, enabled: false })}
|
||||
>
|
||||
Disable Membership Fees
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Alpha Notice */}
|
||||
<Box rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
|
||||
<Stack rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
<Text weight="bold" color="text-warning-amber">Alpha Note:</Text> Membership fee collection is demonstration-only.
|
||||
In production, fees are collected via payment gateway and deposited to league wallet (minus platform fee).
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Link } from '@/ui/Link';
|
||||
|
||||
@@ -18,7 +17,7 @@ interface LeagueNavTabsProps {
|
||||
|
||||
export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
|
||||
return (
|
||||
<Box as="nav" borderBottom borderColor="zinc-800" mb={6}>
|
||||
<Stack as="nav" borderBottom borderColor="zinc-800" mb={6}>
|
||||
<Stack as="ul" direction="row" gap={8} overflow="auto" hideScrollbar>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.exact
|
||||
@@ -26,7 +25,7 @@ export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
|
||||
: currentPathname.startsWith(tab.href);
|
||||
|
||||
return (
|
||||
<Box as="li" key={tab.href} position="relative">
|
||||
<Stack as="li" key={tab.href} position="relative">
|
||||
<Link
|
||||
href={tab.href}
|
||||
variant="ghost"
|
||||
@@ -41,7 +40,7 @@ export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
|
||||
{tab.label}
|
||||
</Link>
|
||||
{isActive && (
|
||||
<Box
|
||||
<Stack
|
||||
position="absolute"
|
||||
bottom="0"
|
||||
left="0"
|
||||
@@ -50,10 +49,10 @@ export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
|
||||
bg="bg-blue-500"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import { Button } from '@/ui/Button';
|
||||
import { UserCog } from 'lucide-react';
|
||||
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
|
||||
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Select } from '@/ui/Select';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
@@ -48,10 +47,10 @@ export function LeagueOwnershipTransfer({
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
{/* League Owner */}
|
||||
<Surface variant="muted" rounded="xl" border padding={5}>
|
||||
<Box mb={3}>
|
||||
<Card variant="outline" p={5} rounded="xl" className="bg-panel-gray/40">
|
||||
<Stack mb={3}>
|
||||
<Heading level={3}>League Owner</Heading>
|
||||
</Box>
|
||||
</Stack>
|
||||
{ownerSummary ? (
|
||||
<DriverSummaryPill
|
||||
driver={new DriverViewModel({
|
||||
@@ -69,20 +68,20 @@ export function LeagueOwnershipTransfer({
|
||||
) : (
|
||||
<Text size="sm" color="text-gray-500">Loading owner details...</Text>
|
||||
)}
|
||||
</Surface>
|
||||
</Card>
|
||||
|
||||
{/* Transfer Ownership - Owner Only */}
|
||||
{settings.league.ownerId === currentDriverId && settings.members.length > 0 && (
|
||||
<Surface variant="muted" rounded="xl" border padding={5}>
|
||||
<Card variant="outline" p={5} rounded="xl" className="bg-panel-gray/40">
|
||||
<Stack direction="row" align="center" gap={2} mb={3}>
|
||||
<Icon icon={UserCog} size={4} color="text-gray-400" />
|
||||
<Heading level={3}>Transfer Ownership</Heading>
|
||||
</Stack>
|
||||
<Box mb={4}>
|
||||
<Stack mb={4}>
|
||||
<Text size="xs" color="text-gray-500">
|
||||
Transfer league ownership to another active member. You will become an admin.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{!showTransferDialog ? (
|
||||
<Button
|
||||
@@ -126,7 +125,7 @@ export function LeagueOwnershipTransfer({
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Surface>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -20,11 +20,12 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
|
||||
interface LeagueReviewSummaryProps {
|
||||
form: LeagueConfigFormModel;
|
||||
@@ -46,17 +47,17 @@ function ReviewCard({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4}>
|
||||
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40" p={4}>
|
||||
<Stack gap={3}>
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box display="flex" h="9" w="9" alignItems="center" justifyContent="center" rounded="lg" bg={bgColor}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Stack h="9" w="9" align="center" justify="center" rounded="lg" bg={bgColor}>
|
||||
<Icon icon={icon} size={4} color={iconColor} />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-white">{title}</Heading>
|
||||
</Box>
|
||||
</Stack>
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,21 +74,19 @@ function InfoRow({
|
||||
valueClass?: string;
|
||||
}) {
|
||||
return (
|
||||
<Box display="flex" alignItems="center" justifyContent="between" py={2} borderBottom borderColor="border-charcoal-outline/20"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
<Stack direction="row" align="center" justify="between" py={2} borderBottom borderColor="border-charcoal-outline/20"
|
||||
className="last:border-0"
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
{icon && <Icon icon={icon} size={3.5} color="text-gray-500" />}
|
||||
<Text size="xs" color="text-gray-500">{label}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="sm" weight="medium" color="text-white"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className={valueClass}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,34 +185,32 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
|
||||
{/* League Summary */}
|
||||
<Stack gap={3}>
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-gray-300">League summary</Heading>
|
||||
<Box position="relative" rounded="2xl" bg="bg-gradient-to-br from-primary-blue/20 via-iron-gray to-iron-gray" border borderColor="border-primary-blue/30" p={6} overflow="hidden">
|
||||
<Stack position="relative" rounded="2xl" bg="bg-gradient-to-br from-primary-blue/20 via-iron-gray to-iron-gray" border borderColor="border-primary-blue/30" p={6} overflow="hidden">
|
||||
{/* Background decoration */}
|
||||
<Box position="absolute" top="0" right="0" w="32" h="32" bg="bg-primary-blue/10" rounded="full"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
<Stack position="absolute" top={0} right={0} w="32" h="32" bg="bg-primary-blue/10" rounded="full"
|
||||
className="blur-3xl"
|
||||
/>
|
||||
<Box position="absolute" bottom="0" left="0" w="24" h="24" bg="bg-neon-aqua/5" rounded="full"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
>{null}</Stack>
|
||||
<Stack position="absolute" bottom="0" left="0" w="24" h="24" bg="bg-neon-aqua/5" rounded="full"
|
||||
className="blur-2xl"
|
||||
/>
|
||||
>{null}</Stack>
|
||||
|
||||
<Box position="relative" display="flex" alignItems="start" gap={4}>
|
||||
<Box display="flex" h="14" w="14" alignItems="center" justifyContent="center" rounded="2xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" flexShrink={0}>
|
||||
<Stack position="relative" direction="row" align="start" gap={4}>
|
||||
<Stack h="14" w="14" align="center" justify="center" rounded="2xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" flexShrink={0}>
|
||||
<Icon icon={Rocket} size={7} color="text-primary-blue" />
|
||||
</Box>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
</Stack>
|
||||
<Stack flexGrow={1} style={{ minWidth: 0 }}>
|
||||
<Heading level={2} fontSize="xl" weight="bold" color="text-white" mb={1} truncate>
|
||||
{basics.name || 'Your New League'}
|
||||
</Heading>
|
||||
<Text size="sm" color="text-gray-400" mb={3} block>
|
||||
{basics.description || 'Ready to launch your racing series!'}
|
||||
</Text>
|
||||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={3}>
|
||||
<Stack direction="row" wrap align="center" gap={3}>
|
||||
{/* Ranked/Unranked Badge */}
|
||||
<Box
|
||||
<Stack
|
||||
as="span"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
align="center"
|
||||
gap={1.5}
|
||||
rounded="full"
|
||||
px={3}
|
||||
@@ -226,31 +223,30 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
|
||||
<Icon icon={isRanked ? Trophy : Users} size={3} />
|
||||
<Text weight="semibold" size="xs">{visibilityLabel}</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
opacity={0.7}
|
||||
>
|
||||
• {visibilityDescription}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
|
||||
</Stack>
|
||||
<Stack as="span" direction="row" align="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
|
||||
<Icon icon={Gamepad2} size={3} />
|
||||
<Text size="xs" weight="medium">iRacing</Text>
|
||||
</Box>
|
||||
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
|
||||
</Stack>
|
||||
<Stack as="span" direction="row" align="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
|
||||
<Icon icon={structure.mode === 'solo' ? User : UsersRound} size={3} />
|
||||
<Text size="xs" weight="medium">{modeLabel}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Season Summary */}
|
||||
<Stack gap={3}>
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-gray-300">First season summary</Heading>
|
||||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||||
<Stack direction="row" wrap align="center" gap={2}>
|
||||
<Text size="xs" color="text-gray-400">{seasonName || 'First season of this league'}</Text>
|
||||
{seasonStartLabel && (
|
||||
<>
|
||||
@@ -266,51 +262,51 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
|
||||
)}
|
||||
<Text size="xs" color="text-gray-400">•</Text>
|
||||
<Text size="xs" color="text-gray-400">Stewarding: {stewardingLabel}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{/* Stats Grid */}
|
||||
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={3}>
|
||||
<Grid cols={2} mdCols={4} gap={3}>
|
||||
{/* Capacity */}
|
||||
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
|
||||
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-primary-blue/10" mx="auto" mb={2}>
|
||||
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
|
||||
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-primary-blue/10" mx="auto" mb={2}>
|
||||
<Icon icon={Users} size={5} color="text-primary-blue" />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>{capacityValue}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>{capacityLabel}</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Rounds */}
|
||||
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
|
||||
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-performance-green/10" mx="auto" mb={2}>
|
||||
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
|
||||
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-performance-green/10" mx="auto" mb={2}>
|
||||
<Icon icon={Flag} size={5} color="text-performance-green" />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>{timings.roundsPlanned ?? '—'}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>rounds</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Weekend Duration */}
|
||||
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
|
||||
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-warning-amber/10" mx="auto" mb={2}>
|
||||
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
|
||||
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-warning-amber/10" mx="auto" mb={2}>
|
||||
<Icon icon={Timer} size={5} color="text-warning-amber" />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>{totalWeekendMinutes > 0 ? `${totalWeekendMinutes}` : '—'}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>min/weekend</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Championships */}
|
||||
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
|
||||
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-neon-aqua/10" mx="auto" mb={2}>
|
||||
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
|
||||
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-neon-aqua/10" mx="auto" mb={2}>
|
||||
<Icon icon={Award} size={5} color="text-neon-aqua" />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>
|
||||
{[championships.enableDriverChampionship, championships.enableTeamChampionship, championships.enableNationsChampionship, championships.enableTrophyChampionship].filter(Boolean).length}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block>championships</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
{/* Detail Cards Grid */}
|
||||
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={4}>
|
||||
<Grid cols={1} mdCols={2} gap={4}>
|
||||
{/* Schedule Card */}
|
||||
<ReviewCard icon={Calendar} title="Race Weekend">
|
||||
<Stack gap={1}>
|
||||
@@ -329,91 +325,90 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
|
||||
<ReviewCard icon={Trophy} iconColor="text-warning-amber" bgColor="bg-warning-amber/10" title="Scoring System">
|
||||
<Stack gap={3}>
|
||||
{/* Scoring Preset */}
|
||||
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Stack direction="row" align="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Text size="2xl">{getScoringEmoji()}</Text>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Stack flexGrow={1} style={{ minWidth: 0 }}>
|
||||
<Text size="sm" weight="medium" color="text-white" block>{preset?.name ?? 'Custom'}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>{preset?.sessionSummary ?? 'Custom scoring enabled'}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{scoring.customScoringEnabled && (
|
||||
<Box as="span" px={2} py={0.5} rounded="sm" bg="bg-primary-blue/20">
|
||||
<Stack as="span" px={2} py={0.5} rounded="sm" bg="bg-primary-blue/20">
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
weight="medium"
|
||||
color="text-primary-blue"
|
||||
>
|
||||
Custom
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Drop Rule */}
|
||||
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Box display="flex" h="8" w="8" alignItems="center" justifyContent="center" rounded="lg" bg="bg-charcoal-outline/50">
|
||||
<Stack direction="row" align="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||||
<Stack h="8" w="8" align="center" justify="center" rounded="lg" bg="bg-charcoal-outline/50">
|
||||
<Text size="base">{dropRuleInfo.emoji}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
</Stack>
|
||||
<Stack flexGrow={1} style={{ minWidth: 0 }}>
|
||||
<Text size="sm" weight="medium" color="text-white" block>{dropRuleInfo.label}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>{dropRuleInfo.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ReviewCard>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Championships Section */}
|
||||
<ReviewCard icon={Award} iconColor="text-neon-aqua" bgColor="bg-neon-aqua/10" title="Active Championships">
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
<Stack direction="row" wrap gap={2}>
|
||||
{championships.enableDriverChampionship && (
|
||||
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Icon icon={Trophy} size={3.5} color="text-primary-blue" />
|
||||
<Text size="xs" weight="medium" color="text-primary-blue">Driver Championship</Text>
|
||||
<Icon icon={Check} size={3} color="text-performance-green" />
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{championships.enableTeamChampionship && (
|
||||
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Icon icon={Award} size={3.5} color="text-primary-blue" />
|
||||
<Text size="xs" weight="medium" color="text-primary-blue">Team Championship</Text>
|
||||
<Icon icon={Check} size={3} color="text-performance-green" />
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{championships.enableNationsChampionship && (
|
||||
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Icon icon={Globe} size={3.5} color="text-primary-blue" />
|
||||
<Text size="xs" weight="medium" color="text-primary-blue">Nations Cup</Text>
|
||||
<Icon icon={Check} size={3} color="text-performance-green" />
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{championships.enableTrophyChampionship && (
|
||||
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
|
||||
<Icon icon={Medal} size={3.5} color="text-primary-blue" />
|
||||
<Text size="xs" weight="medium" color="text-primary-blue">Trophy Championship</Text>
|
||||
<Icon icon={Check} size={3} color="text-performance-green" />
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{![championships.enableDriverChampionship, championships.enableTeamChampionship, championships.enableNationsChampionship, championships.enableTrophyChampionship].some(Boolean) && (
|
||||
<Text size="sm" color="text-gray-500">No championships enabled</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</ReviewCard>
|
||||
|
||||
{/* Ready to launch message */}
|
||||
<Box rounded="xl" bg="bg-performance-green/5" border borderColor="border-performance-green/20" p={4}>
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-performance-green/20">
|
||||
<Card rounded="xl" className="bg-performance-green/5 border-performance-green/20" p={4}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-performance-green/20">
|
||||
<Icon icon={Check} size={5} color="text-performance-green" />
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" weight="medium" color="text-white" block>Ready to launch!</Text>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
Click "Create League" to launch your racing series. You can modify all settings later.
|
||||
Click "Create League" to launch your racing series. You can modify all settings later.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -19,10 +18,10 @@ interface LeagueRulesPanelProps {
|
||||
|
||||
export function LeagueRulesPanel({ rules }: LeagueRulesPanelProps) {
|
||||
return (
|
||||
<Box as="section">
|
||||
<Stack as="section">
|
||||
<Stack gap={8}>
|
||||
<Box display="flex" alignItems="start" gap={4} p={4} bg="blue-500/5" border borderColor="blue-500/20">
|
||||
<Box color="text-blue-500" mt={0.5}><Info size={20} /></Box>
|
||||
<Stack display="flex" alignItems="start" gap={4} p={4} bg="blue-500/5" border borderColor="blue-500/20">
|
||||
<Stack color="text-blue-500" mt={0.5}><Info size={20} /></Stack>
|
||||
<Stack gap={1}>
|
||||
<Text size="sm" weight="bold" color="text-blue-500" uppercase letterSpacing="0.05em">Code of Conduct</Text>
|
||||
<Text size="sm" color="text-zinc-400" leading="relaxed">
|
||||
@@ -30,24 +29,24 @@ export function LeagueRulesPanel({ rules }: LeagueRulesPanelProps) {
|
||||
Intentional wrecking or abusive behavior will result in immediate disqualification.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
<Stack display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
{rules.map((rule) => (
|
||||
<Box as="article" key={rule.id} display="flex" flexDirection="col" gap={3} p={6} border borderColor="zinc-800" bg="zinc-900/30">
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box p={2} bg="zinc-800" color="text-zinc-400">
|
||||
<Stack as="article" key={rule.id} display="flex" flexDirection="col" gap={3} p={6} border borderColor="zinc-800" bg="zinc-900/30">
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack p={2} bg="zinc-800" color="text-zinc-400">
|
||||
<Shield size={18} />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Heading level={3} fontSize="md" weight="bold" color="text-white">{rule.title}</Heading>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="sm" color="text-zinc-400" leading="relaxed">
|
||||
{rule.content}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,8 @@ import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueSchedu
|
||||
import { StateContainer } from '@/components/shared/state/StateContainer';
|
||||
import { useLeagueSchedule } from "@/hooks/league/useLeagueSchedule";
|
||||
import { Calendar } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
@@ -102,11 +101,11 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
{/* Filter Controls */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="sm" color="text-gray-400">
|
||||
{displayRaces.length} {displayRaces.length === 1 ? 'race' : 'races'}
|
||||
</Text>
|
||||
<Box display="flex" gap={2}>
|
||||
<Stack display="flex" gap={2}>
|
||||
<Button
|
||||
variant={filter === 'upcoming' ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
@@ -128,17 +127,17 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
|
||||
>
|
||||
All ({races.length})
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Race List */}
|
||||
{displayRaces.length === 0 ? (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Stack textAlign="center" py={8}>
|
||||
<Text color="text-gray-400" block mb={2}>No {filter} races</Text>
|
||||
{filter === 'upcoming' && (
|
||||
<Text size="sm" color="text-gray-500" block>Schedule your first race to get started</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack gap={3}>
|
||||
{displayRaces.map((race) => {
|
||||
@@ -152,7 +151,7 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
|
||||
registerMutation.isPending || withdrawMutation.isPending;
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={race.id}
|
||||
p={4}
|
||||
rounded="lg"
|
||||
@@ -166,34 +165,34 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
|
||||
opacity={isPast ? 0.75 : 1}
|
||||
onClick={() => onRaceClick?.(race.id)}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" gap={4}>
|
||||
<Box flexGrow={1}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={1} flexWrap="wrap">
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" gap={4}>
|
||||
<Stack flexGrow={1}>
|
||||
<Stack display="flex" alignItems="center" gap={2} mb={1} flexWrap="wrap">
|
||||
<Heading level={3} fontSize="base" weight="medium" color="text-white">{trackLabel}</Heading>
|
||||
{isUpcoming && !isRegistered && (
|
||||
<Box as="span" px={2} py={0.5} bg="bg-primary-blue/10" border borderColor="border-primary-blue/30" rounded="sm">
|
||||
<Stack as="span" px={2} py={0.5} bg="bg-primary-blue/10" border borderColor="border-primary-blue/30" rounded="sm">
|
||||
<Text size="xs" weight="medium" color="text-primary-blue">Upcoming</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{isUpcoming && isRegistered && (
|
||||
<Box as="span" px={2} py={0.5} bg="bg-green-500/10" border borderColor="border-green-500/30" rounded="sm">
|
||||
<Stack as="span" px={2} py={0.5} bg="bg-green-500/10" border borderColor="border-green-500/30" rounded="sm">
|
||||
<Text size="xs" weight="medium" color="text-green-400">✓ Registered</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{isPast && (
|
||||
<Box as="span" px={2} py={0.5} bg="bg-gray-700/50" border borderColor="border-gray-600/50" rounded="sm">
|
||||
<Stack as="span" px={2} py={0.5} bg="bg-gray-700/50" border borderColor="border-gray-600/50" rounded="sm">
|
||||
<Text size="xs" weight="medium" color="text-gray-400">Completed</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="sm" color="text-gray-400" block>{carLabel}</Text>
|
||||
<Box mt={2}>
|
||||
<Stack mt={2}>
|
||||
<Text size="xs" color="text-gray-500" transform="uppercase">{sessionTypeLabel}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box textAlign="right">
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack textAlign="right">
|
||||
<Text color="text-white" weight="medium" block>
|
||||
{race.scheduledAt.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
@@ -210,11 +209,11 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
|
||||
{isPast && race.status === 'completed' && (
|
||||
<Text size="xs" color="text-primary-blue" mt={1} block>View Results →</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Registration Actions */}
|
||||
{isUpcoming && (
|
||||
<Box onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<Stack onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
{!isRegistered ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -239,11 +238,11 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
|
||||
{withdrawMutation.isPending ? 'Withdrawing...' : 'Withdraw'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -22,10 +21,10 @@ interface LeagueSchedulePanelProps {
|
||||
|
||||
export function LeagueSchedulePanel({ events }: LeagueSchedulePanelProps) {
|
||||
return (
|
||||
<Box as="section">
|
||||
<Stack as="section">
|
||||
<Stack gap={4}>
|
||||
{events.map((event) => (
|
||||
<Box
|
||||
<Stack
|
||||
as="article"
|
||||
key={event.id}
|
||||
display="flex"
|
||||
@@ -38,56 +37,56 @@ export function LeagueSchedulePanel({ events }: LeagueSchedulePanelProps) {
|
||||
hoverBorderColor="zinc-700"
|
||||
transition
|
||||
>
|
||||
<Box display="flex" flexDirection="col" alignItems="center" justifyContent="center" w="16" h="16" borderRight borderColor="zinc-800" pr={6}>
|
||||
<Stack display="flex" flexDirection="col" alignItems="center" justifyContent="center" w="16" h="16" borderRight borderColor="zinc-800" pr={6}>
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase>
|
||||
{new Date(event.date).toLocaleDateString('en-US', { month: 'short' })}
|
||||
</Text>
|
||||
<Text size="2xl" weight="bold" color="text-white">
|
||||
{new Date(event.date).toLocaleDateString('en-US', { day: 'numeric' })}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Heading level={3} fontSize="lg" weight="bold" color="text-white">{event.title}</Heading>
|
||||
<Stack direction="row" gap={4} mt={1}>
|
||||
<Box display="flex" alignItems="center" gap={1.5}>
|
||||
<Box color="text-zinc-600"><MapPin size={14} /></Box>
|
||||
<Stack display="flex" alignItems="center" gap={1.5}>
|
||||
<Stack color="text-zinc-600"><MapPin size={14} /></Stack>
|
||||
<Text size="sm" color="text-zinc-400">{event.trackName}</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1.5}>
|
||||
<Box color="text-zinc-600"><Clock size={14} /></Box>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={1.5}>
|
||||
<Stack color="text-zinc-600"><Clock size={14} /></Stack>
|
||||
<Text size="sm" color="text-zinc-400">{event.time}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
{event.status === 'live' && (
|
||||
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} bg="red-500/10" border borderColor="red-500/20">
|
||||
<Box w="1.5" h="1.5" rounded="full" bg="bg-red-500" animate="pulse" />
|
||||
<Stack display="flex" alignItems="center" gap={1.5} px={2} py={1} bg="red-500/10" border borderColor="red-500/20">
|
||||
<Stack w="1.5" h="1.5" rounded="full" bg="bg-red-500" animate="pulse" />
|
||||
<Text size="xs" weight="bold" color="text-red-500" uppercase letterSpacing="0.05em">
|
||||
Live
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{event.status === 'upcoming' && (
|
||||
<Box px={2} py={1} bg="blue-500/10" border borderColor="blue-500/20">
|
||||
<Stack px={2} py={1} bg="blue-500/10" border borderColor="blue-500/20">
|
||||
<Text size="xs" weight="bold" color="text-blue-500" uppercase letterSpacing="0.05em">
|
||||
Upcoming
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{event.status === 'completed' && (
|
||||
<Box px={2} py={1} bg="zinc-800" border borderColor="zinc-700">
|
||||
<Stack px={2} py={1} bg="zinc-800" border borderColor="zinc-700">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="0.05em">
|
||||
Results
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import { createPortal } from 'react-dom';
|
||||
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
@@ -98,7 +97,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
if (!isOpen) return null;
|
||||
|
||||
return createPortal(
|
||||
<Box
|
||||
<Stack
|
||||
ref={flyoutRef}
|
||||
position="fixed"
|
||||
zIndex={50}
|
||||
@@ -113,7 +112,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
style={{ top: position.top, left: position.left }}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between" p={4} borderBottom borderColor="border-charcoal-outline" position="sticky" top="0" bg="bg-iron-gray" zIndex={10}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" p={4} borderBottom borderColor="border-charcoal-outline" position="sticky" top="0" bg="bg-iron-gray" zIndex={10}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
|
||||
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
|
||||
@@ -127,12 +126,12 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
>
|
||||
<Icon icon={X} size={4} color="text-gray-400" />
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
{/* Content */}
|
||||
<Box p={4}>
|
||||
<Stack p={4}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>,
|
||||
</Stack>
|
||||
</Stack>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -168,7 +167,7 @@ function PointsSystemMockup() {
|
||||
return (
|
||||
<Surface variant="dark" rounded="lg" p={4}>
|
||||
<Stack gap={3}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" px={1}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" px={1}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -189,27 +188,27 @@ function PointsSystemMockup() {
|
||||
>
|
||||
Points
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{positions.map((p) => (
|
||||
<Stack key={p.pos} direction="row" align="center" gap={3}>
|
||||
<Box w="8" h="8" rounded="lg" bg={p.color} display="flex" alignItems="center" justifyContent="center">
|
||||
<Stack w="8" h="8" rounded="lg" bg={p.color} display="flex" alignItems="center" justifyContent="center">
|
||||
<Text size="sm" weight="bold" color={p.pos <= 3 ? 'text-deep-graphite' : 'text-gray-400'}>P{p.pos}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="full" overflow="hidden" opacity={0.5}>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="full" overflow="hidden" opacity={0.5}>
|
||||
<Stack
|
||||
h="full"
|
||||
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
|
||||
rounded="full"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ width: `${(p.pts / 25) * 100}%` }}
|
||||
/>
|
||||
</Box>
|
||||
<Box w="8" textAlign="right">
|
||||
</Stack>
|
||||
<Stack w="8" textAlign="right">
|
||||
<Text size="sm" font="mono" weight="semibold" color="text-white">{p.pts}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
<Box display="flex" alignItems="center" justifyContent="center" gap={1} pt={2}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="center" gap={1} pt={2}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -224,7 +223,7 @@ function PointsSystemMockup() {
|
||||
>
|
||||
down to P10 = 1 point
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
@@ -244,7 +243,7 @@ function BonusPointsMockup() {
|
||||
<Surface key={i} variant="muted" border rounded="lg" p={2}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Text size="xl">{b.emoji}</Text>
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Text size="xs" weight="medium" color="text-white" block>{b.label}</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -254,7 +253,7 @@ function BonusPointsMockup() {
|
||||
>
|
||||
{b.desc}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="sm" font="mono" weight="semibold" color="text-performance-green">{b.pts}</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
@@ -273,14 +272,14 @@ function ChampionshipMockup() {
|
||||
|
||||
return (
|
||||
<Surface variant="dark" rounded="lg" p={4}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline" opacity={0.5}>
|
||||
<Stack display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline" opacity={0.5}>
|
||||
<Icon icon={Trophy} size={4} color="text-yellow-500" />
|
||||
<Text size="xs" weight="semibold" color="text-white">Driver Championship</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack gap={2}>
|
||||
{standings.map((s) => (
|
||||
<Stack key={s.pos} direction="row" align="center" gap={2}>
|
||||
<Box w="6" h="6" rounded="full" display="flex" alignItems="center" justifyContent="center" bg={s.pos === 1 ? 'bg-yellow-500' : 'bg-charcoal-outline'} color={s.pos === 1 ? 'text-deep-graphite' : 'text-gray-400'}>
|
||||
<Stack w="6" h="6" rounded="full" display="flex" alignItems="center" justifyContent="center" bg={s.pos === 1 ? 'bg-yellow-500' : 'bg-charcoal-outline'} color={s.pos === 1 ? 'text-deep-graphite' : 'text-gray-400'}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -288,10 +287,10 @@ function ChampionshipMockup() {
|
||||
>
|
||||
{s.pos}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
</Stack>
|
||||
<Stack flexGrow={1}>
|
||||
<Text size="xs" color="text-white" truncate block>{s.name}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="xs" font="mono" weight="semibold" color="text-white">{s.pts}</Text>
|
||||
{s.delta && (
|
||||
<Text
|
||||
@@ -306,7 +305,7 @@ function ChampionshipMockup() {
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Box mt={3} pt={2} borderTop borderColor="border-charcoal-outline" opacity={0.5} textAlign="center">
|
||||
<Stack mt={3} pt={2} borderTop borderColor="border-charcoal-outline" opacity={0.5} textAlign="center">
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -314,7 +313,7 @@ function ChampionshipMockup() {
|
||||
>
|
||||
Points accumulated across all races
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
@@ -419,11 +418,11 @@ export function LeagueScoringSection({
|
||||
const championshipsPanel = <ChampionshipsSection {...championshipsProps} />;
|
||||
|
||||
if (patternOnly) {
|
||||
return <Box>{patternPanel}</Box>;
|
||||
return <Stack>{patternPanel}</Stack>;
|
||||
}
|
||||
|
||||
if (championshipsOnly) {
|
||||
return <Box>{championshipsPanel}</Box>;
|
||||
return <Stack>{championshipsPanel}</Stack>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -572,16 +571,16 @@ export function ScoringPatternSection({
|
||||
<Stack gap={5}>
|
||||
{/* Section header */}
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
|
||||
<Stack w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
|
||||
<Icon icon={Trophy} size={5} color="text-primary-blue" />
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
</Stack>
|
||||
<Stack flexGrow={1}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Heading level={3}>Points System</Heading>
|
||||
<InfoButton buttonRef={pointsInfoRef} onClick={() => setShowPointsFlyout(true)} />
|
||||
</Stack>
|
||||
<Text size="xs" color="text-gray-500">Choose how points are awarded</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Points System Flyout */}
|
||||
@@ -597,8 +596,8 @@ export function ScoringPatternSection({
|
||||
which accumulate across the season to determine championship standings.
|
||||
</Text>
|
||||
|
||||
<Box>
|
||||
<Box mb={2}>
|
||||
<Stack>
|
||||
<Stack mb={2}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -609,9 +608,9 @@ export function ScoringPatternSection({
|
||||
>
|
||||
Example: F1-Style Points
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<PointsSystemMockup />
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Surface variant="muted" border rounded="lg" p={3}>
|
||||
<Stack direction="row" align="start" gap={2}>
|
||||
@@ -640,15 +639,15 @@ export function ScoringPatternSection({
|
||||
{/* Preset options */}
|
||||
<Stack gap={2}>
|
||||
{presets.length === 0 ? (
|
||||
<Box p={4} border borderStyle="dashed" borderColor="border-charcoal-outline" rounded="lg">
|
||||
<Stack p={4} border borderStyle="dashed" borderColor="border-charcoal-outline" rounded="lg">
|
||||
<Text size="sm" color="text-gray-400">Loading presets...</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
presets.map((preset) => {
|
||||
const isSelected = !isCustom && scoring.patternId === preset.id;
|
||||
const presetInfo = getPresetInfoContent(preset.name);
|
||||
return (
|
||||
<Box key={preset.id} position="relative">
|
||||
<Stack key={preset.id} position="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChangePatternId?.(preset.id)}
|
||||
@@ -663,7 +662,7 @@ export function ScoringPatternSection({
|
||||
`}
|
||||
>
|
||||
{/* Radio indicator */}
|
||||
<Box
|
||||
<Stack
|
||||
w="5"
|
||||
h="5"
|
||||
display="flex"
|
||||
@@ -677,33 +676,33 @@ export function ScoringPatternSection({
|
||||
flexShrink={0}
|
||||
>
|
||||
{isSelected && <Icon icon={Check} size={3} color="text-white" />}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Emoji */}
|
||||
<Text size="xl">{getPresetEmoji(preset)}</Text>
|
||||
|
||||
{/* Text */}
|
||||
<Box flexGrow={1}
|
||||
<Stack flexGrow={1}
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="min-w-0"
|
||||
>
|
||||
<Text size="sm" weight="medium" color="text-white" block>{preset.name}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>{getPresetDescription(preset)}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Bonus badge */}
|
||||
{preset.bonusSummary && (
|
||||
<Box
|
||||
<Stack
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="hidden sm:inline-flex items-center gap-1 px-2 py-1 rounded-full bg-charcoal-outline/30 text-[10px] text-gray-400"
|
||||
>
|
||||
<Icon icon={Zap} size={3} />
|
||||
<Text>{preset.bonusSummary}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Info button */}
|
||||
<Box
|
||||
<Stack
|
||||
ref={(el: HTMLElement | null) => { presetInfoRefs.current[preset.id] = el; }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -731,7 +730,7 @@ export function ScoringPatternSection({
|
||||
flexShrink={0}
|
||||
>
|
||||
<Icon icon={HelpCircle} size={3.5} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Button>
|
||||
|
||||
{/* Preset Info Flyout */}
|
||||
@@ -745,7 +744,7 @@ export function ScoringPatternSection({
|
||||
<Text size="xs" color="text-gray-400">{presetInfo.description}</Text>
|
||||
|
||||
<Stack gap={2}>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -756,21 +755,21 @@ export function ScoringPatternSection({
|
||||
>
|
||||
Key Features
|
||||
</Text>
|
||||
</Box>
|
||||
<Box as="ul"
|
||||
</Stack>
|
||||
<Stack as="ul"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="space-y-1.5"
|
||||
>
|
||||
{presetInfo.details.map((detail, idx) => (
|
||||
<Box as="li" key={idx} display="flex" alignItems="start" gap={2}>
|
||||
<Stack as="li" key={idx} display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3} color="text-performance-green"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<Text size="xs" color="text-gray-400">{detail}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{preset.bonusSummary && (
|
||||
@@ -792,14 +791,14 @@ export function ScoringPatternSection({
|
||||
)}
|
||||
</Stack>
|
||||
</InfoFlyout>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Custom scoring option */}
|
||||
<Box w="full"
|
||||
<Stack w="full"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="lg:w-48"
|
||||
>
|
||||
@@ -816,7 +815,7 @@ export function ScoringPatternSection({
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
<Stack
|
||||
w="10"
|
||||
h="10"
|
||||
display="flex"
|
||||
@@ -828,8 +827,8 @@ export function ScoringPatternSection({
|
||||
transition
|
||||
>
|
||||
<Icon icon={Settings} size={5} color={isCustom ? 'text-primary-blue' : 'text-gray-500'} />
|
||||
</Box>
|
||||
<Box textAlign="center">
|
||||
</Stack>
|
||||
<Stack textAlign="center">
|
||||
<Text size="sm" weight="medium" color={isCustom ? 'text-white' : 'text-gray-400'} block>Custom</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -839,9 +838,9 @@ export function ScoringPatternSection({
|
||||
>
|
||||
Define your own
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{isCustom && (
|
||||
<Box display="flex" alignItems="center" gap={1} px={2} py={0.5} rounded="full" bg="bg-primary-blue" opacity={0.2}>
|
||||
<Stack display="flex" alignItems="center" gap={1} px={2} py={0.5} rounded="full" bg="bg-primary-blue" opacity={0.2}>
|
||||
<Icon icon={Check} size={2.5} color="text-primary-blue" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -851,10 +850,10 @@ export function ScoringPatternSection({
|
||||
>
|
||||
Active
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* Error message */}
|
||||
@@ -867,7 +866,7 @@ export function ScoringPatternSection({
|
||||
<Surface variant="muted" border rounded="xl" p={4}>
|
||||
<Stack gap={4}>
|
||||
{/* Header with reset button */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Settings} size={4} color="text-primary-blue" />
|
||||
<Text size="sm" weight="medium" color="text-white">Custom Points Table</Text>
|
||||
@@ -887,8 +886,8 @@ export function ScoringPatternSection({
|
||||
They add strategic depth and excitement to your championship.
|
||||
</Text>
|
||||
|
||||
<Box>
|
||||
<Box mb={2}>
|
||||
<Stack>
|
||||
<Stack mb={2}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -899,9 +898,9 @@ export function ScoringPatternSection({
|
||||
>
|
||||
Available Bonuses
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<BonusPointsMockup />
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Surface variant="muted" border rounded="lg" p={3}>
|
||||
<Stack direction="row" align="start" gap={2}>
|
||||
@@ -935,11 +934,11 @@ export function ScoringPatternSection({
|
||||
<Text>Reset</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Race position points */}
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-400">Finish position points</Text>
|
||||
<Stack direction="row" align="center" gap={1}>
|
||||
<Button
|
||||
@@ -963,9 +962,9 @@ export function ScoringPatternSection({
|
||||
<Icon icon={Plus} size={3} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box display="flex" flexWrap="wrap" gap={1}>
|
||||
<Stack display="flex" flexWrap="wrap" gap={1}>
|
||||
{customPoints.racePoints.map((pts, idx) => (
|
||||
<Stack key={idx} align="center">
|
||||
<Text
|
||||
@@ -976,7 +975,7 @@ export function ScoringPatternSection({
|
||||
>
|
||||
P{idx + 1}
|
||||
</Text>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Stack display="flex" alignItems="center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -987,7 +986,7 @@ export function ScoringPatternSection({
|
||||
>
|
||||
−
|
||||
</Button>
|
||||
<Box w="6" h="5" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
|
||||
<Stack w="6" h="5" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -996,7 +995,7 @@ export function ScoringPatternSection({
|
||||
>
|
||||
{pts}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -1007,10 +1006,10 @@ export function ScoringPatternSection({
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Bonus points */}
|
||||
@@ -1028,7 +1027,7 @@ export function ScoringPatternSection({
|
||||
>
|
||||
{bonus.emoji} {bonus.label}
|
||||
</Text>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Stack display="flex" alignItems="center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -1039,9 +1038,9 @@ export function ScoringPatternSection({
|
||||
>
|
||||
−
|
||||
</Button>
|
||||
<Box w="7" h="6" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
|
||||
<Stack w="7" h="6" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
|
||||
<Text size="xs" weight="medium" color="text-white">{customPoints[bonus.key]}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -1052,7 +1051,7 @@ export function ScoringPatternSection({
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Grid>
|
||||
@@ -1174,16 +1173,16 @@ export function ChampionshipsSection({
|
||||
<Stack gap={4}>
|
||||
{/* Section header */}
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
|
||||
<Stack w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
|
||||
<Icon icon={Award} size={5} color="text-primary-blue" />
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
</Stack>
|
||||
<Stack flexGrow={1}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Heading level={3}>Championships</Heading>
|
||||
<InfoButton buttonRef={champInfoRef} onClick={() => setShowChampFlyout(true)} />
|
||||
</Stack>
|
||||
<Text size="xs" color="text-gray-500">What standings to track</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Championships Flyout */}
|
||||
@@ -1199,8 +1198,8 @@ export function ChampionshipsSection({
|
||||
championship types to run different competitions simultaneously.
|
||||
</Text>
|
||||
|
||||
<Box>
|
||||
<Box mb={2}>
|
||||
<Stack>
|
||||
<Stack mb={2}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -1211,12 +1210,12 @@ export function ChampionshipsSection({
|
||||
>
|
||||
Live Standings Example
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<ChampionshipMockup />
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={2}>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -1227,7 +1226,7 @@ export function ChampionshipsSection({
|
||||
>
|
||||
Championship Types
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Grid cols={2} gap={2}>
|
||||
{[
|
||||
{ icon: Trophy, label: 'Driver', desc: 'Individual points' },
|
||||
@@ -1238,7 +1237,7 @@ export function ChampionshipsSection({
|
||||
<Surface key={i} variant="dark" border rounded="lg" p={2}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={t.icon} size={3.5} color="text-primary-blue" />
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -1256,7 +1255,7 @@ export function ChampionshipsSection({
|
||||
>
|
||||
{t.desc}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Surface>
|
||||
))}
|
||||
@@ -1272,7 +1271,7 @@ export function ChampionshipsSection({
|
||||
const champInfo = CHAMPIONSHIP_INFO[champ.key];
|
||||
|
||||
return (
|
||||
<Box key={champ.key} position="relative">
|
||||
<Stack key={champ.key} position="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
disabled={disabled || !champ.available}
|
||||
@@ -1289,7 +1288,7 @@ export function ChampionshipsSection({
|
||||
`}
|
||||
>
|
||||
{/* Toggle indicator */}
|
||||
<Box
|
||||
<Stack
|
||||
w="5"
|
||||
h="5"
|
||||
display="flex"
|
||||
@@ -1302,7 +1301,7 @@ export function ChampionshipsSection({
|
||||
flexShrink={0}
|
||||
>
|
||||
{isEnabled && <Icon icon={Check} size={3} color="text-white" />}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Icon */}
|
||||
<Icon icon={champ.icon} size={4} color={isEnabled ? 'text-primary-blue' : 'text-gray-500'}
|
||||
@@ -1311,7 +1310,7 @@ export function ChampionshipsSection({
|
||||
/>
|
||||
|
||||
{/* Text */}
|
||||
<Box flexGrow={1}
|
||||
<Stack flexGrow={1}
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="min-w-0"
|
||||
>
|
||||
@@ -1333,10 +1332,10 @@ export function ChampionshipsSection({
|
||||
{champ.unavailableHint}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Info button */}
|
||||
<Box
|
||||
<Stack
|
||||
ref={(el: HTMLElement | null) => { champItemRefs.current[champ.key] = el; }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1364,7 +1363,7 @@ export function ChampionshipsSection({
|
||||
flexShrink={0}
|
||||
>
|
||||
<Icon icon={HelpCircle} size={3} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Button>
|
||||
|
||||
{/* Championship Item Info Flyout */}
|
||||
@@ -1379,7 +1378,7 @@ export function ChampionshipsSection({
|
||||
<Text size="xs" color="text-gray-400">{champInfo.description}</Text>
|
||||
|
||||
<Stack gap={2}>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -1390,21 +1389,21 @@ export function ChampionshipsSection({
|
||||
>
|
||||
How It Works
|
||||
</Text>
|
||||
</Box>
|
||||
<Box as="ul"
|
||||
</Stack>
|
||||
<Stack as="ul"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="space-y-1.5"
|
||||
>
|
||||
{champInfo.details.map((detail, idx) => (
|
||||
<Box as="li" key={idx} display="flex" alignItems="start" gap={2}>
|
||||
<Stack as="li" key={idx} display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3} color="text-performance-green"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<Text size="xs" color="text-gray-400">{detail}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{!champ.available && (
|
||||
@@ -1430,7 +1429,7 @@ export function ChampionshipsSection({
|
||||
</Stack>
|
||||
</InfoFlyout>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -55,20 +54,20 @@ export function LeagueSlider({
|
||||
if (leagues.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box mb={10}>
|
||||
<Stack mb={10}>
|
||||
{/* Section header */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={4}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" mb={4}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box display="flex" h={10} w={10} alignItems="center" justifyContent="center" rounded="xl" bg="bg-iron-gray" border borderColor="border-charcoal-outline">
|
||||
<Stack display="flex" h={10} w={10} alignItems="center" justifyContent="center" rounded="xl" bg="bg-iron-gray" border borderColor="border-charcoal-outline">
|
||||
<Icon icon={IconComp} size={5} color={iconColor} />
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Heading level={2}>{title}</Heading>
|
||||
<Text size="xs" color="text-gray-500">{description}</Text>
|
||||
</Box>
|
||||
<Box as="span" ml={2} px={2} py={0.5} rounded="full" fontSize="0.75rem" bg="bg-charcoal-outline/50" color="text-gray-400">
|
||||
</Stack>
|
||||
<Stack as="span" ml={2} px={2} py={0.5} rounded="full" fontSize="0.75rem" bg="bg-charcoal-outline/50" color="text-gray-400">
|
||||
{leagues.length}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Navigation arrows */}
|
||||
@@ -94,11 +93,11 @@ export function LeagueSlider({
|
||||
<Icon icon={ChevronRight} size={4} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Scrollable container with fade edges */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
<Stack position="relative">
|
||||
<Stack
|
||||
position="absolute"
|
||||
top={0}
|
||||
bottom={4}
|
||||
@@ -108,7 +107,7 @@ export function LeagueSlider({
|
||||
zIndex={10}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Box
|
||||
<Stack
|
||||
position="absolute"
|
||||
top={0}
|
||||
bottom={4}
|
||||
@@ -119,7 +118,7 @@ export function LeagueSlider({
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
<Box
|
||||
<Stack
|
||||
ref={scrollRef}
|
||||
onScroll={checkScrollButtons}
|
||||
display="flex"
|
||||
@@ -133,15 +132,15 @@ export function LeagueSlider({
|
||||
const viewModel = LeagueSummaryViewModelBuilder.build(league);
|
||||
|
||||
return (
|
||||
<Box key={league.id} flexShrink={0} w="320px">
|
||||
<Stack key={league.id} flexShrink={0} w="320px">
|
||||
<Link href={routes.league.detail(league.id)} block>
|
||||
<LeagueCard league={viewModel} />
|
||||
</Link>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ import { useState } from 'react';
|
||||
import { PendingSponsorshipRequests } from '../sponsors/PendingSponsorshipRequests';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { StatBox } from '@/ui/StatBox';
|
||||
@@ -120,8 +119,8 @@ export function LeagueSponsorshipsSection({
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack>
|
||||
<Heading level={3}>Sponsorships</Heading>
|
||||
<Text size="sm" color="text-gray-400" mt={1} block>
|
||||
Define pricing for sponsor slots in this league. Sponsors pay per season.
|
||||
@@ -129,20 +128,20 @@ export function LeagueSponsorshipsSection({
|
||||
<Text size="xs" color="text-gray-500" mt={1} block>
|
||||
These sponsors are attached to seasons in this league, so you can change partners from season to season.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{!readOnly && (
|
||||
<Box display="flex" alignItems="center" gap={2} px={3} py={1.5} rounded="full" bg="bg-primary-blue/10" border borderColor="border-primary-blue/30">
|
||||
<Stack display="flex" alignItems="center" gap={2} px={3} py={1.5} rounded="full" bg="bg-primary-blue/10" border borderColor="border-primary-blue/30">
|
||||
<Icon icon={DollarSign} size={4} color="var(--primary-blue)" />
|
||||
<Text size="xs" weight="medium" color="text-primary-blue">
|
||||
{availableSlots} slot{availableSlots !== 1 ? 's' : ''} available
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Revenue Summary */}
|
||||
{totalRevenue > 0 && (
|
||||
<Box display="grid" gridCols={3} gap={4}>
|
||||
<Stack display="grid" gridCols={3} gap={4}>
|
||||
<StatBox
|
||||
icon={DollarSign}
|
||||
label="Total Revenue"
|
||||
@@ -161,7 +160,7 @@ export function LeagueSponsorshipsSection({
|
||||
value={`$${netRevenue.toFixed(2)}`}
|
||||
color="var(--performance-green)"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Sponsorship Slots */}
|
||||
@@ -171,7 +170,7 @@ export function LeagueSponsorshipsSection({
|
||||
const IconComp = slot.tier === 'main' ? Star : Award;
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={index}
|
||||
rounded="lg"
|
||||
border
|
||||
@@ -179,9 +178,9 @@ export function LeagueSponsorshipsSection({
|
||||
bg="bg-deep-graphite/70"
|
||||
p={4}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" gap={4}>
|
||||
<Box display="flex" alignItems="center" gap={3} flexGrow={1}>
|
||||
<Box
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" gap={4}>
|
||||
<Stack display="flex" alignItems="center" gap={3} flexGrow={1}>
|
||||
<Stack
|
||||
display="flex"
|
||||
h="10"
|
||||
w="10"
|
||||
@@ -191,10 +190,10 @@ export function LeagueSponsorshipsSection({
|
||||
bg={slot.tier === 'main' ? 'bg-primary-blue/10' : 'bg-gray-500/10'}
|
||||
>
|
||||
<Icon icon={IconComp} size={5} color={slot.tier === 'main' ? 'var(--primary-blue)' : 'var(--gray-400)'} />
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box flexGrow={1}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack flexGrow={1}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Heading level={4}>
|
||||
{slot.tier === 'main' ? 'Main Sponsor' : 'Secondary Sponsor'}
|
||||
</Heading>
|
||||
@@ -203,18 +202,18 @@ export function LeagueSponsorshipsSection({
|
||||
Occupied
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="xs" color="text-gray-500" mt={0.5} block>
|
||||
{slot.tier === 'main'
|
||||
? 'Big livery slot • League page logo • Name in league title'
|
||||
: 'Small livery slot • League page logo'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
{isEditing ? (
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Input
|
||||
type="number"
|
||||
value={tempPrice}
|
||||
@@ -239,15 +238,15 @@ export function LeagueSponsorshipsSection({
|
||||
>
|
||||
<Icon icon={X} size={4} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<Box textAlign="right">
|
||||
<Stack textAlign="right">
|
||||
<Text size="lg" weight="bold" color="text-white" block>
|
||||
${slot.price.toFixed(2)}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block>per season</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{!readOnly && !slot.isOccupied && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -259,16 +258,16 @@ export function LeagueSponsorshipsSection({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
{/* Pending Sponsorship Requests */}
|
||||
{!readOnly && (pendingRequests.length > 0 || requestsLoading) && (
|
||||
<Box mt={8} pt={6} borderTop borderColor="border-charcoal-outline">
|
||||
<Stack mt={8} pt={6} borderTop borderColor="border-charcoal-outline">
|
||||
<PendingSponsorshipRequests
|
||||
entityType="season"
|
||||
entityId={seasonId || ''}
|
||||
@@ -278,16 +277,16 @@ export function LeagueSponsorshipsSection({
|
||||
onReject={handleRejectRequest}
|
||||
isLoading={requestsLoading}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Alpha Notice */}
|
||||
<Box rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
|
||||
<Stack rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
<Text weight="bold" color="text-warning-amber">Alpha Note:</Text> Sponsorship management is demonstration-only.
|
||||
In production, sponsors can browse leagues, select slots, and complete payment integration.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface StandingEntry {
|
||||
position: number;
|
||||
@@ -20,61 +21,61 @@ interface LeagueStandingsTableProps {
|
||||
|
||||
export function LeagueStandingsTable({ standings }: LeagueStandingsTableProps) {
|
||||
return (
|
||||
<Box as="section" overflow="hidden" border borderColor="zinc-800" bg="zinc-900/50">
|
||||
<Box as="table" w="full" textAlign="left">
|
||||
<Box as="thead">
|
||||
<Box as="tr" borderBottom borderColor="zinc-800" bg="zinc-900/80">
|
||||
<Box as="th" px={4} py={3} w="12">
|
||||
<Stack as="section" overflow="hidden" border borderColor="zinc-800" bg="zinc-900/50">
|
||||
<Table className="border-none rounded-none">
|
||||
<TableHead>
|
||||
<TableRow className="bg-zinc-900/80 border-zinc-800">
|
||||
<TableHeader className="w-12 px-4 py-3">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Pos</Text>
|
||||
</Box>
|
||||
<Box as="th" px={4} py={3}>
|
||||
</TableHeader>
|
||||
<TableHeader className="px-4 py-3">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Driver</Text>
|
||||
</Box>
|
||||
<Box as="th" px={4} py={3} display={{ base: 'none', md: 'table-cell' }}>
|
||||
</TableHeader>
|
||||
<TableHeader className="hidden md:table-cell px-4 py-3">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Team</Text>
|
||||
</Box>
|
||||
<Box as="th" px={4} py={3} textAlign="center">
|
||||
</TableHeader>
|
||||
<TableHeader className="text-center px-4 py-3">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Wins</Text>
|
||||
</Box>
|
||||
<Box as="th" px={4} py={3} textAlign="center">
|
||||
</TableHeader>
|
||||
<TableHeader className="text-center px-4 py-3">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Podiums</Text>
|
||||
</Box>
|
||||
<Box as="th" px={4} py={3} textAlign="right">
|
||||
</TableHeader>
|
||||
<TableHeader className="text-right px-4 py-3">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Points</Text>
|
||||
</Box>
|
||||
<Box as="th" px={4} py={3} textAlign="right">
|
||||
</TableHeader>
|
||||
<TableHeader className="text-right px-4 py-3">
|
||||
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Gap</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="tbody">
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody className="divide-zinc-800">
|
||||
{standings.map((entry) => (
|
||||
<Box as="tr" key={entry.driverName} borderBottom borderColor="zinc-800" hoverBg="zinc-800/50" transition>
|
||||
<Box as="td" px={4} py={3}>
|
||||
<TableRow key={entry.driverName} className="border-zinc-800 hover:bg-zinc-800/50">
|
||||
<TableCell className="px-4 py-3">
|
||||
<Text size="sm" color="text-zinc-400" font="mono">{entry.position}</Text>
|
||||
</Box>
|
||||
<Box as="td" px={4} py={3}>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-3">
|
||||
<Text size="sm" weight="medium" color="text-zinc-200">{entry.driverName}</Text>
|
||||
</Box>
|
||||
<Box as="td" px={4} py={3} display={{ base: 'none', md: 'table-cell' }}>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell px-4 py-3">
|
||||
<Text size="sm" color="text-zinc-500">{entry.teamName || '—'}</Text>
|
||||
</Box>
|
||||
<Box as="td" px={4} py={3} textAlign="center">
|
||||
</TableCell>
|
||||
<TableCell className="text-center px-4 py-3">
|
||||
<Text size="sm" color="text-zinc-400">{entry.wins}</Text>
|
||||
</Box>
|
||||
<Box as="td" px={4} py={3} textAlign="center">
|
||||
</TableCell>
|
||||
<TableCell className="text-center px-4 py-3">
|
||||
<Text size="sm" color="text-zinc-400">{entry.podiums}</Text>
|
||||
</Box>
|
||||
<Box as="td" px={4} py={3} textAlign="right">
|
||||
</TableCell>
|
||||
<TableCell className="text-right px-4 py-3">
|
||||
<Text size="sm" weight="bold" color="text-white">{entry.points}</Text>
|
||||
</Box>
|
||||
<Box as="td" px={4} py={3} textAlign="right">
|
||||
</TableCell>
|
||||
<TableCell className="text-right px-4 py-3">
|
||||
<Text size="sm" color="text-zinc-500" font="mono">{entry.gap}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import React from 'react';
|
||||
import { Scale, Clock, Bell, Shield, Vote, AlertTriangle } from 'lucide-react';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Select } from '@/ui/Select';
|
||||
@@ -75,7 +74,7 @@ export function LeagueStewardingSection({
|
||||
return (
|
||||
<Stack gap={8}>
|
||||
{/* Decision Mode Selection */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Scale} size={4} color="text-primary-blue" />
|
||||
@@ -86,9 +85,9 @@ export function LeagueStewardingSection({
|
||||
Choose who has the authority to issue penalties
|
||||
</Text>
|
||||
|
||||
<Box display="grid" gridCols={{ base: 1, sm: 2, lg: 3 }} gap={3}>
|
||||
<Stack display="grid" gridCols={{ base: 1, sm: 2, lg: 3 }} gap={3}>
|
||||
{decisionModeOptions.map((option) => (
|
||||
<Box
|
||||
<Stack
|
||||
key={option.value}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -112,29 +111,29 @@ export function LeagueStewardingSection({
|
||||
opacity={readOnly ? 0.6 : 1}
|
||||
cursor={readOnly ? 'not-allowed' : 'pointer'}
|
||||
>
|
||||
<Box
|
||||
<Stack
|
||||
p={2}
|
||||
rounded="lg"
|
||||
bg={stewarding.decisionMode === option.value ? 'bg-primary-blue/20' : 'bg-charcoal-outline/50'}
|
||||
color={stewarding.decisionMode === option.value ? 'text-primary-blue' : 'text-gray-400'}
|
||||
>
|
||||
{option.icon}
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" weight="medium" color="text-white" block>{option.label}</Text>
|
||||
<Text size="xs" color="text-gray-400" mt={0.5} block>{option.description}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{stewarding.decisionMode === option.value && (
|
||||
<Box position="absolute" top="2" right="2" w="2" h="2" rounded="full" bg="bg-primary-blue" />
|
||||
<Stack position="absolute" top="2" right="2" w="2" h="2" rounded="full" bg="bg-primary-blue" />
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Vote Requirements (conditional) */}
|
||||
{selectedMode?.requiresVotes && (
|
||||
<Box p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Stack p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Stack gap={4}>
|
||||
<Heading level={4} fontSize="sm" weight="medium" color="text-white">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
@@ -143,8 +142,8 @@ export function LeagueStewardingSection({
|
||||
</Stack>
|
||||
</Heading>
|
||||
|
||||
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
|
||||
<Box>
|
||||
<Stack display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
|
||||
<Stack>
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
|
||||
Required votes to uphold
|
||||
</Text>
|
||||
@@ -160,9 +159,9 @@ export function LeagueStewardingSection({
|
||||
{ value: '5', label: '5 votes' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
|
||||
Voting time limit
|
||||
</Text>
|
||||
@@ -178,14 +177,14 @@ export function LeagueStewardingSection({
|
||||
{ value: '168', label: '168 hours (7 days)' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Defense Settings */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Shield} size={4} color="text-primary-blue" />
|
||||
@@ -196,8 +195,8 @@ export function LeagueStewardingSection({
|
||||
Should accused drivers be required to submit a defense?
|
||||
</Text>
|
||||
|
||||
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={3}>
|
||||
<Box
|
||||
<Stack display="grid" gridCols={{ base: 1, sm: 2 }} gap={3}>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={readOnly}
|
||||
@@ -217,16 +216,16 @@ export function LeagueStewardingSection({
|
||||
opacity={readOnly ? 0.6 : 1}
|
||||
cursor={readOnly ? 'not-allowed' : 'pointer'}
|
||||
>
|
||||
<Box w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={!stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
|
||||
{!stewarding.requireDefense && <Box w="2" h="2" rounded="full" bg="bg-primary-blue" />}
|
||||
</Box>
|
||||
<Box>
|
||||
<Stack w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={!stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
|
||||
{!stewarding.requireDefense && <Stack w="2" h="2" rounded="full" bg="bg-primary-blue" />}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" weight="medium" color="text-white" block>Defense optional</Text>
|
||||
<Text size="xs" color="text-gray-400" block>Proceed without waiting for defense</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={readOnly}
|
||||
@@ -246,18 +245,18 @@ export function LeagueStewardingSection({
|
||||
opacity={readOnly ? 0.6 : 1}
|
||||
cursor={readOnly ? 'not-allowed' : 'pointer'}
|
||||
>
|
||||
<Box w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
|
||||
{stewarding.requireDefense && <Box w="2" h="2" rounded="full" bg="bg-primary-blue" />}
|
||||
</Box>
|
||||
<Box>
|
||||
<Stack w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
|
||||
{stewarding.requireDefense && <Stack w="2" h="2" rounded="full" bg="bg-primary-blue" />}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" weight="medium" color="text-white" block>Defense required</Text>
|
||||
<Text size="xs" color="text-gray-400" block>Wait for defense before deciding</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{stewarding.requireDefense && (
|
||||
<Box mt={4} p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Stack mt={4} p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
|
||||
Defense time limit
|
||||
</Text>
|
||||
@@ -275,12 +274,12 @@ export function LeagueStewardingSection({
|
||||
<Text size="xs" color="text-gray-500" mt={2} block>
|
||||
After this time, the decision can proceed without a defense
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Deadlines */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Clock} size={4} color="text-primary-blue" />
|
||||
@@ -291,8 +290,8 @@ export function LeagueStewardingSection({
|
||||
Set time limits for filing protests and closing stewarding
|
||||
</Text>
|
||||
|
||||
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
|
||||
<Box p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Stack display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
|
||||
<Stack p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
|
||||
Protest filing deadline (after race)
|
||||
</Text>
|
||||
@@ -311,9 +310,9 @@ export function LeagueStewardingSection({
|
||||
<Text size="xs" color="text-gray-500" mt={2} block>
|
||||
Drivers cannot file protests after this time
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Stack p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
|
||||
Stewarding closes (after race)
|
||||
</Text>
|
||||
@@ -331,12 +330,12 @@ export function LeagueStewardingSection({
|
||||
<Text size="xs" color="text-gray-500" mt={2} block>
|
||||
All stewarding must be concluded by this time
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Notifications */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Bell} size={4} color="text-primary-blue" />
|
||||
@@ -348,7 +347,7 @@ export function LeagueStewardingSection({
|
||||
</Text>
|
||||
|
||||
<Stack gap={3}>
|
||||
<Box
|
||||
<Stack
|
||||
p={4}
|
||||
rounded="xl"
|
||||
bg="bg-iron-gray/40"
|
||||
@@ -364,14 +363,14 @@ export function LeagueStewardingSection({
|
||||
onChange={(checked) => updateStewarding({ notifyAccusedOnProtest: checked })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Box ml={7} mt={1}>
|
||||
<Stack ml={7} mt={1}>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
Send notification when a protest is filed against them
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
<Stack
|
||||
p={4}
|
||||
rounded="xl"
|
||||
bg="bg-iron-gray/40"
|
||||
@@ -387,27 +386,27 @@ export function LeagueStewardingSection({
|
||||
onChange={(checked) => updateStewarding({ notifyOnVoteRequired: checked })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<Box ml={7} mt={1}>
|
||||
<Stack ml={7} mt={1}>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
Send notification to stewards/members when their vote is needed
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Warning about strict settings */}
|
||||
{stewarding.requireDefense && stewarding.decisionMode !== 'single_steward' && (
|
||||
<Box display="flex" alignItems="start" gap={3} p={4} rounded="xl" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
|
||||
<Stack display="flex" alignItems="start" gap={3} p={4} rounded="xl" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
|
||||
<Icon icon={AlertTriangle} size={5} color="text-warning-amber" mt={0.5} />
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="sm" weight="medium" color="text-warning-amber" block>Strict settings enabled</Text>
|
||||
<Text size="xs" color="text-warning-amber" opacity={0.8} mt={1} block>
|
||||
Requiring defense and voting may delay penalty decisions. Make sure your stewards/members
|
||||
are active enough to meet the deadlines.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -5,10 +5,9 @@ import { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import type * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
|
||||
@@ -85,7 +84,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
if (!isOpen || !mounted) return null;
|
||||
|
||||
return createPortal(
|
||||
<Box
|
||||
<Stack
|
||||
ref={flyoutRef}
|
||||
position="fixed"
|
||||
zIndex={50}
|
||||
@@ -98,7 +97,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
|
||||
>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
@@ -114,7 +113,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
|
||||
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
|
||||
</Stack>
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -128,12 +127,12 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
hoverBg="bg-charcoal-outline"
|
||||
>
|
||||
<Icon icon={X} size={4} color="text-gray-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack p={4}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>,
|
||||
</Stack>
|
||||
</Stack>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -281,20 +280,20 @@ export function LeagueStructureSection({
|
||||
return (
|
||||
<Stack gap={8}>
|
||||
{/* Emotional header */}
|
||||
<Box textAlign="center" pb={2}>
|
||||
<Stack textAlign="center" pb={2}>
|
||||
<Heading level={3} mb={2}>
|
||||
How will your drivers compete?
|
||||
</Heading>
|
||||
<Text size="sm" color="text-gray-400" maxWidth="lg" mx="auto" block>
|
||||
Choose your championship format — individual glory or team triumph.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Mode Selection Cards */}
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={4}>
|
||||
<Stack display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={4}>
|
||||
{/* Solo Mode Card */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
<Stack position="relative">
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
@@ -319,9 +318,9 @@ export function LeagueStructureSection({
|
||||
group
|
||||
>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="12"
|
||||
w="12"
|
||||
@@ -331,18 +330,18 @@ export function LeagueStructureSection({
|
||||
bg={isSolo ? 'bg-primary-blue/30' : 'bg-charcoal-outline/50'}
|
||||
>
|
||||
<Icon icon={User} size={6} color={isSolo ? 'text-primary-blue' : 'text-gray-400'} />
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text weight="bold" size="lg" color={isSolo ? 'text-white' : 'text-gray-300'} block>
|
||||
Solo Drivers
|
||||
</Text>
|
||||
<Text size="xs" color={isSolo ? 'text-primary-blue' : 'text-gray-500'} block>
|
||||
Individual competition
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{/* Radio indicator */}
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="6"
|
||||
w="6"
|
||||
@@ -356,8 +355,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
{isSolo && <Icon icon={Check} size={3.5} color="text-white" />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Emotional tagline */}
|
||||
<Text size="sm" color={isSolo ? 'text-gray-300' : 'text-gray-500'} block>
|
||||
@@ -379,10 +378,10 @@ export function LeagueStructureSection({
|
||||
<Text size="xs" color="text-gray-400">Perfect for any grid size</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Info button */}
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
ref={soloInfoRef}
|
||||
type="button"
|
||||
@@ -402,8 +401,8 @@ export function LeagueStructureSection({
|
||||
hoverBg="bg-primary-blue/10"
|
||||
>
|
||||
<Icon icon={HelpCircle} size={4} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Solo Info Flyout */}
|
||||
<InfoFlyout
|
||||
@@ -445,8 +444,8 @@ export function LeagueStructureSection({
|
||||
</InfoFlyout>
|
||||
|
||||
{/* Teams Mode Card */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
<Stack position="relative">
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
@@ -471,9 +470,9 @@ export function LeagueStructureSection({
|
||||
group
|
||||
>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="12"
|
||||
w="12"
|
||||
@@ -483,18 +482,18 @@ export function LeagueStructureSection({
|
||||
bg={!isSolo ? 'bg-neon-aqua/30' : 'bg-charcoal-outline/50'}
|
||||
>
|
||||
<Icon icon={Users2} size={6} color={!isSolo ? 'text-neon-aqua' : 'text-gray-400'} />
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text weight="bold" size="lg" color={!isSolo ? 'text-white' : 'text-gray-300'} block>
|
||||
Team Racing
|
||||
</Text>
|
||||
<Text size="xs" color={!isSolo ? 'text-neon-aqua' : 'text-gray-500'} block>
|
||||
Shared destiny
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{/* Radio indicator */}
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="6"
|
||||
w="6"
|
||||
@@ -508,8 +507,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
{!isSolo && <Icon icon={Check} size={3.5} color="text-deep-graphite" />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Emotional tagline */}
|
||||
<Text size="sm" color={!isSolo ? 'text-gray-300' : 'text-gray-500'} block>
|
||||
@@ -531,10 +530,10 @@ export function LeagueStructureSection({
|
||||
<Text size="xs" color="text-gray-400">Great for endurance & pro-am</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Info button */}
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
ref={teamsInfoRef}
|
||||
type="button"
|
||||
@@ -554,8 +553,8 @@ export function LeagueStructureSection({
|
||||
hoverBg="bg-neon-aqua/10"
|
||||
>
|
||||
<Icon icon={HelpCircle} size={4} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Teams Info Flyout */}
|
||||
<InfoFlyout
|
||||
@@ -595,13 +594,13 @@ export function LeagueStructureSection({
|
||||
</Stack>
|
||||
</Stack>
|
||||
</InfoFlyout>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Configuration Panel */}
|
||||
<Box rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={6}>
|
||||
<Stack rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={6}>
|
||||
<Stack gap={5}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="10"
|
||||
w="10"
|
||||
@@ -615,8 +614,8 @@ export function LeagueStructureSection({
|
||||
) : (
|
||||
<Icon icon={Users2} size={5} color="text-neon-aqua" />
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" weight="semibold" color="text-white" block>
|
||||
{isSolo ? 'Grid size' : 'Team configuration'}
|
||||
</Text>
|
||||
@@ -626,7 +625,7 @@ export function LeagueStructureSection({
|
||||
: 'Configure teams and roster sizes'
|
||||
}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Solo mode capacity */}
|
||||
@@ -653,8 +652,8 @@ export function LeagueStructureSection({
|
||||
|
||||
<Stack gap={2}>
|
||||
<Text size="xs" color="text-gray-500" block>Quick select:</Text>
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
<Box
|
||||
<Stack display="flex" flexWrap="wrap" gap={2}>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => handleMaxDriversChange('16')}
|
||||
@@ -671,8 +670,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
Compact (16)
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => handleMaxDriversChange('24')}
|
||||
@@ -689,8 +688,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
Standard (24)
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => handleMaxDriversChange('30')}
|
||||
@@ -707,8 +706,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
Full Grid (30)
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => handleMaxDriversChange('40')}
|
||||
@@ -725,8 +724,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
Large (40)
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => handleMaxDriversChange(String(gameConstraints.maxDrivers))}
|
||||
@@ -743,8 +742,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
Max ({gameConstraints.maxDrivers})
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -755,8 +754,8 @@ export function LeagueStructureSection({
|
||||
{/* Quick presets */}
|
||||
<Stack gap={3}>
|
||||
<Text size="xs" color="text-gray-500" block>Popular configurations:</Text>
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
<Box
|
||||
<Stack display="flex" flexWrap="wrap" gap={2}>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -776,8 +775,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
10 × 2 (20 grid)
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -797,8 +796,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
12 × 2 (24 grid)
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -818,8 +817,8 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
8 × 3 (24 grid)
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -839,12 +838,12 @@ export function LeagueStructureSection({
|
||||
transition
|
||||
>
|
||||
15 × 2 (30 grid)
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Manual configuration */}
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, sm: 3 }} gap={4} pt={2} borderTop borderColor="border-charcoal-outline/50">
|
||||
<Stack display="grid" responsiveGridCols={{ base: 1, sm: 3 }} gap={4} pt={2} borderTop borderColor="border-charcoal-outline/50">
|
||||
<Stack gap={2}>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
|
||||
Teams
|
||||
@@ -877,7 +876,7 @@ export function LeagueStructureSection({
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
|
||||
Total grid
|
||||
</Text>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
@@ -891,13 +890,13 @@ export function LeagueStructureSection({
|
||||
{structure.maxDrivers ?? 0}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" ml={1}>drivers</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
|
||||
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
@@ -9,9 +6,8 @@ import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { LeagueLogo } from './LeagueLogo';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface LeagueSummaryCardProps {
|
||||
id: string;
|
||||
@@ -32,10 +28,10 @@ export function LeagueSummaryCard({
|
||||
}: LeagueSummaryCardProps) {
|
||||
return (
|
||||
<Card p={0} style={{ overflow: 'hidden' }}>
|
||||
<Box p={4}>
|
||||
<Stack p={4}>
|
||||
<Stack direction="row" align="center" gap={4} mb={4}>
|
||||
<LeagueLogo leagueId={id} alt={name} size={56} />
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text
|
||||
size="xs"
|
||||
color="text-gray-500"
|
||||
@@ -48,7 +44,7 @@ export function LeagueSummaryCard({
|
||||
<Heading level={3} style={{ fontSize: '1rem' }}>
|
||||
{name}
|
||||
</Heading>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{description && (
|
||||
@@ -64,17 +60,17 @@ export function LeagueSummaryCard({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box mb={4}>
|
||||
<Stack mb={4}>
|
||||
<Grid cols={2} gap={3}>
|
||||
<Surface variant="dark" rounded="lg" padding={3}>
|
||||
<Card variant="outline" rounded="lg" p={3} className="bg-graphite-black">
|
||||
<Text size="xs" color="text-gray-500" block mb={1}>
|
||||
Max Drivers
|
||||
</Text>
|
||||
<Text weight="medium" color="text-white">
|
||||
{maxDrivers}
|
||||
</Text>
|
||||
</Surface>
|
||||
<Surface variant="dark" rounded="lg" padding={3}>
|
||||
</Card>
|
||||
<Card variant="outline" rounded="lg" p={3} className="bg-graphite-black">
|
||||
<Text size="xs" color="text-gray-500" block mb={1}>
|
||||
Format
|
||||
</Text>
|
||||
@@ -85,11 +81,11 @@ export function LeagueSummaryCard({
|
||||
>
|
||||
{qualifyingFormat}
|
||||
</Text>
|
||||
</Surface>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack>
|
||||
<Link href={href}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -99,8 +95,8 @@ export function LeagueSummaryCard({
|
||||
View League
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Text } from '@/ui/Text';
|
||||
@@ -18,7 +17,7 @@ interface LeagueTabsProps {
|
||||
|
||||
export function LeagueTabs({ tabs }: LeagueTabsProps) {
|
||||
return (
|
||||
<Box borderBottom borderColor="border-charcoal-outline">
|
||||
<Stack borderBottom borderColor="border-charcoal-outline">
|
||||
<Stack direction="row" gap={6} overflow="auto">
|
||||
{tabs.map((tab) => (
|
||||
<Link
|
||||
@@ -26,17 +25,17 @@ export function LeagueTabs({ tabs }: LeagueTabsProps) {
|
||||
href={tab.href}
|
||||
variant="ghost"
|
||||
>
|
||||
<Box pb={3} px={1}>
|
||||
<Stack pb={3} px={1}>
|
||||
<Text weight="medium"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import type { Weekday } from '@/lib/types/Weekday';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { RangeField } from '@/components/shared/RangeField';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
@@ -119,7 +118,7 @@ function RaceDayPreview({
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Flag} size={3.5} color="text-primary-blue" />
|
||||
<Text size="xs" weight="medium" color="text-white">Race Day Schedule</Text>
|
||||
<Text size="xs" color="text-gray-600">•</Text>
|
||||
@@ -127,7 +126,7 @@ function RaceDayPreview({
|
||||
<Text size="xs" color="text-gray-400">
|
||||
Starts {effectiveRaceTime}{!raceTime && <Text as="span" color="text-gray-600" ml={1}>(default)</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Timeline visualization - show ALL sessions */}
|
||||
<Stack gap={2}>
|
||||
@@ -138,7 +137,7 @@ function RaceDayPreview({
|
||||
const startTime = isActive ? getStartTime(activeIndex) : null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={session.name}
|
||||
position="relative"
|
||||
display="flex"
|
||||
@@ -157,7 +156,7 @@ function RaceDayPreview({
|
||||
>
|
||||
{/* Status badge */}
|
||||
{!isActive && (
|
||||
<Box position="absolute" top="-1" right="-1" px={1.5} py={0.5} rounded="sm" bg="bg-charcoal-outline">
|
||||
<Stack position="absolute" top="-1" right="-1" px={1.5} py={0.5} rounded="sm" bg="bg-charcoal-outline">
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '8px' }}
|
||||
@@ -166,21 +165,21 @@ function RaceDayPreview({
|
||||
>
|
||||
Not included
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Time marker */}
|
||||
<Box w="12" textAlign="right" flexShrink={0}>
|
||||
<Stack w="12" textAlign="right" flexShrink={0}>
|
||||
<Text size="xs" font="mono" color={!isActive ? 'text-gray-600' : isRace ? 'text-primary-blue' : 'text-gray-400'}>
|
||||
{startTime ?? '—'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Session indicator */}
|
||||
<Box w="1" h="8" rounded="full" transition bg={!isActive ? 'bg-charcoal-outline/30' : isRace ? 'bg-primary-blue' : 'bg-charcoal-outline'} />
|
||||
<Stack w="1" h="8" rounded="full" transition bg={!isActive ? 'bg-charcoal-outline/30' : isRace ? 'bg-primary-blue' : 'bg-charcoal-outline'} />
|
||||
|
||||
{/* Session info */}
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Text size="sm" weight="medium" color={!isActive ? 'text-gray-600' : isRace ? 'text-white' : 'text-gray-300'} block>
|
||||
{session.name}
|
||||
</Text>
|
||||
@@ -192,29 +191,29 @@ function RaceDayPreview({
|
||||
>
|
||||
{isActive ? `${session.duration} minutes` : 'Disabled'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Duration bar */}
|
||||
{isActive && (
|
||||
<Box w="16" h="2" bg="bg-charcoal-outline/50" rounded="full" overflow="hidden">
|
||||
<Box
|
||||
<Stack w="16" h="2" bg="bg-charcoal-outline/50" rounded="full" overflow="hidden">
|
||||
<Stack
|
||||
h="full"
|
||||
rounded="full"
|
||||
bg={isRace ? 'bg-primary-blue' : 'bg-gray-600'}
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ width: `${(session.duration / Math.max(...activeSessions.map(s => s.duration))) * 100}%` }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
{/* Legend */}
|
||||
<Box display="flex" alignItems="center" justifyContent="center" gap={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
|
||||
<Box display="flex" alignItems="center" gap={1.5}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-primary-blue" />
|
||||
<Stack display="flex" alignItems="center" justifyContent="center" gap={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
|
||||
<Stack display="flex" alignItems="center" gap={1.5}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-primary-blue" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -222,9 +221,9 @@ function RaceDayPreview({
|
||||
>
|
||||
Active race
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1.5}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-gray-600" />
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={1.5}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-gray-600" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -232,9 +231,9 @@ function RaceDayPreview({
|
||||
>
|
||||
Active session
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1.5}>
|
||||
<Box w="2" h="2" rounded="sm" border borderStyle="dashed" borderColor="border-charcoal-outline/50" bg="transparent" />
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={1.5}>
|
||||
<Stack w="2" h="2" rounded="sm" border borderStyle="dashed" borderColor="border-charcoal-outline/50" bg="transparent" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -242,12 +241,12 @@ function RaceDayPreview({
|
||||
>
|
||||
Not included
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Summary */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -269,14 +268,14 @@ function RaceDayPreview({
|
||||
>
|
||||
{activeSessions.filter(s => s.type === 'race' || s.type === 'sprint').length} race{activeSessions.filter(s => s.type === 'race' || s.type === 'sprint').length > 1 ? 's' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="full" bg="bg-iron-gray/60">
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="full" bg="bg-iron-gray/60">
|
||||
<Icon icon={Timer} size={3} color="text-primary-blue" />
|
||||
<Text size="xs" weight="semibold" color="text-white">
|
||||
{Math.floor(totalDuration / 60)}h {totalDuration % 60}m
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -565,11 +564,11 @@ function YearCalendarPreview({
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={CalendarRange} size={3.5} color="text-primary-blue" />
|
||||
<Text size="xs" weight="medium" color="text-white">Season Calendar</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -577,17 +576,17 @@ function YearCalendarPreview({
|
||||
>
|
||||
{raceDates.length} race{raceDates.length !== 1 ? 's' : ''} scheduled
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Year grid - 3 columns x 4 rows */}
|
||||
<Box display="grid" gridCols={3} gap={2}>
|
||||
<Stack display="grid" gridCols={3} gap={2}>
|
||||
{yearView.map(({ month, monthIndex, year, days }) => {
|
||||
const hasRaces = days.some(d => d.isRace);
|
||||
const raceCount = days.filter(d => d.isRace).length;
|
||||
const uniqueKey = `${year}-${monthIndex}`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={uniqueKey}
|
||||
rounded="lg"
|
||||
p={2}
|
||||
@@ -596,7 +595,7 @@ function YearCalendarPreview({
|
||||
borderColor={hasRaces ? 'border-primary-blue/30' : 'border-charcoal-outline/30'}
|
||||
bg={hasRaces ? 'bg-primary-blue/5' : 'bg-iron-gray/20'}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={1}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" mb={1}>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -615,17 +614,17 @@ function YearCalendarPreview({
|
||||
{raceCount}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Mini calendar grid */}
|
||||
<Box display="grid" gridCols={7} gap="1px">
|
||||
<Stack display="grid" gridCols={7} gap="1px">
|
||||
{/* Fill empty days at start - getDay() returns 0 for Sunday, we want Monday first */}
|
||||
{Array.from({ length: (new Date(year, monthIndex, 1).getDay() + 6) % 7 }).map((_, i) => (
|
||||
<Box key={`empty-${uniqueKey}-${i}`} w="2" h="2" />
|
||||
<Stack key={`empty-${uniqueKey}-${i}`} w="2" h="2" />
|
||||
))}
|
||||
|
||||
{days.map(({ dayOfMonth, isRace, isStart, isEnd, raceNumber }) => (
|
||||
<Box
|
||||
<Stack
|
||||
key={`${uniqueKey}-${dayOfMonth}`}
|
||||
w="2"
|
||||
h="2"
|
||||
@@ -651,15 +650,15 @@ function YearCalendarPreview({
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Season summary */}
|
||||
<Box display="grid" gridCols={3} gap={2} pt={2} borderTop borderColor="border-charcoal-outline/30">
|
||||
<Box textAlign="center">
|
||||
<Stack display="grid" gridCols={3} gap={2} pt={2} borderTop borderColor="border-charcoal-outline/30">
|
||||
<Stack textAlign="center">
|
||||
<Text size="lg" weight="bold" color="text-white" block>{rounds}</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -669,8 +668,8 @@ function YearCalendarPreview({
|
||||
>
|
||||
Rounds
|
||||
</Text>
|
||||
</Box>
|
||||
<Box textAlign="center">
|
||||
</Stack>
|
||||
<Stack textAlign="center">
|
||||
<Text size="lg" weight="bold" color="text-white" block>
|
||||
{seasonDurationWeeks || '—'}
|
||||
</Text>
|
||||
@@ -682,8 +681,8 @@ function YearCalendarPreview({
|
||||
>
|
||||
Weeks
|
||||
</Text>
|
||||
</Box>
|
||||
<Box textAlign="center">
|
||||
</Stack>
|
||||
<Stack textAlign="center">
|
||||
<Text size="lg" weight="bold" color="text-primary-blue" block>
|
||||
{firstRace && lastRace
|
||||
? `${getMonthLabel(firstRace.getMonth())}–${getMonthLabel(
|
||||
@@ -699,13 +698,13 @@ function YearCalendarPreview({
|
||||
>
|
||||
Duration
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Legend */}
|
||||
<Box display="flex" alignItems="center" justifyContent="center" gap={3} flexWrap="wrap">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-performance-green"
|
||||
<Stack display="flex" alignItems="center" justifyContent="center" gap={3} flexWrap="wrap">
|
||||
<Stack display="flex" alignItems="center" gap={1}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-performance-green"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="ring-1 ring-performance-green"
|
||||
/>
|
||||
@@ -716,9 +715,9 @@ function YearCalendarPreview({
|
||||
>
|
||||
Start
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-primary-blue" />
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={1}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-primary-blue" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -726,10 +725,10 @@ function YearCalendarPreview({
|
||||
>
|
||||
Race
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
{seasonEnd && (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-warning-amber"
|
||||
<Stack display="flex" alignItems="center" gap={1}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-warning-amber"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="ring-1 ring-warning-amber"
|
||||
/>
|
||||
@@ -740,10 +739,10 @@ function YearCalendarPreview({
|
||||
>
|
||||
End
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-charcoal-outline/30" />
|
||||
<Stack display="flex" alignItems="center" gap={1}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-charcoal-outline/30" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -751,8 +750,8 @@ function YearCalendarPreview({
|
||||
>
|
||||
No race
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -790,16 +789,16 @@ function SeasonStatsPreview({
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Trophy} size={3.5} color="text-primary-blue" />
|
||||
<Text size="xs" weight="medium" color="text-white">Season Statistics</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Visual rounds */}
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" alignItems="center" gap={1} flexWrap="wrap">
|
||||
<Stack display="flex" alignItems="center" gap={1} flexWrap="wrap">
|
||||
{Array.from({ length: Math.min(rounds, 20) }).map((_, i) => (
|
||||
<Box
|
||||
<Stack
|
||||
key={i}
|
||||
w="5"
|
||||
h="5"
|
||||
@@ -830,7 +829,7 @@ function SeasonStatsPreview({
|
||||
>
|
||||
{i + 1}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
{rounds > 20 && (
|
||||
<Text
|
||||
@@ -842,10 +841,10 @@ function SeasonStatsPreview({
|
||||
+{rounds - 20}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-performance-green" />
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" alignItems="center" gap={1}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-performance-green" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -853,9 +852,9 @@ function SeasonStatsPreview({
|
||||
>
|
||||
Season start
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-primary-blue" />
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="center" gap={1}>
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-primary-blue" />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ fontSize: '10px' }}
|
||||
@@ -863,13 +862,13 @@ function SeasonStatsPreview({
|
||||
>
|
||||
Finale
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Stats grid */}
|
||||
<Box display="grid" gridCols={2} gap={2}>
|
||||
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
<Stack display="grid" gridCols={2} gap={2}>
|
||||
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>{totalSessions}</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -879,8 +878,8 @@ function SeasonStatsPreview({
|
||||
>
|
||||
Total sessions
|
||||
</Text>
|
||||
</Box>
|
||||
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
</Stack>
|
||||
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>{Math.round(totalRaceMinutes / 60)}h</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -890,8 +889,8 @@ function SeasonStatsPreview({
|
||||
>
|
||||
Racing time
|
||||
</Text>
|
||||
</Box>
|
||||
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
</Stack>
|
||||
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>~{weeksNeeded}</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -901,8 +900,8 @@ function SeasonStatsPreview({
|
||||
>
|
||||
Weeks duration
|
||||
</Text>
|
||||
</Box>
|
||||
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
</Stack>
|
||||
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>{totalMinutesPerRound}</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -912,8 +911,8 @@ function SeasonStatsPreview({
|
||||
>
|
||||
min/race day
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -964,12 +963,12 @@ function InlineEditableRounds({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" gap={4} p={4} rounded="xl"
|
||||
<Stack display="flex" alignItems="center" gap={4} p={4} rounded="xl"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="bg-gradient-to-br from-iron-gray/60 to-iron-gray/30 border border-charcoal-outline"
|
||||
>
|
||||
{isEditing ? (
|
||||
<Box
|
||||
<Stack
|
||||
as="input"
|
||||
ref={inputRef}
|
||||
type="number"
|
||||
@@ -991,7 +990,7 @@ function InlineEditableRounds({
|
||||
className="text-4xl outline-none"
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1013,9 +1012,9 @@ function InlineEditableRounds({
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="group-hover:opacity-100 text-primary-blue transition-opacity"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Text size="sm" weight="medium" color="text-gray-300" block>rounds</Text>
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -1025,9 +1024,9 @@ function InlineEditableRounds({
|
||||
>
|
||||
Click to edit
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" flexDirection="col" gap={1}>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack display="flex" flexDirection="col" gap={1}>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => onChange(Math.min(value + 1, max))}
|
||||
@@ -1043,8 +1042,8 @@ function InlineEditableRounds({
|
||||
cursor={value >= max ? 'not-allowed' : 'pointer'}
|
||||
>
|
||||
<Icon icon={ChevronUp} size={4} color="text-gray-400" />
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => onChange(Math.max(value - 1, min))}
|
||||
@@ -1060,9 +1059,9 @@ function InlineEditableRounds({
|
||||
cursor={value <= min ? 'not-allowed' : 'pointer'}
|
||||
>
|
||||
<Icon icon={ChevronDown} size={4} color="text-gray-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1088,8 +1087,8 @@ function CollapsibleSection({
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<Box rounded="xl" border borderColor="border-charcoal-outline/50" bg="bg-iron-gray/20" overflow="hidden" transition>
|
||||
<Box
|
||||
<Stack rounded="xl" border borderColor="border-charcoal-outline/50" bg="bg-iron-gray/20" overflow="hidden" transition>
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
@@ -1101,36 +1100,36 @@ function CollapsibleSection({
|
||||
transition
|
||||
hoverBg="bg-iron-gray/30"
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box display="flex" h="8" w="8" alignItems="center" justifyContent="center" rounded="lg" bg="bg-primary-blue/10" flexShrink={0}>
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" h="8" w="8" alignItems="center" justifyContent="center" rounded="lg" bg="bg-primary-blue/10" flexShrink={0}>
|
||||
{icon}
|
||||
</Box>
|
||||
<Box textAlign="left">
|
||||
</Stack>
|
||||
<Stack textAlign="left">
|
||||
<Heading level={3} fontSize="sm" weight="semibold" color="text-white">{title}</Heading>
|
||||
{description && (
|
||||
<Text size="xs" color="text-gray-500" mt={0.5} block>{description}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box transform transition
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack transform transition
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className={isOpen ? 'rotate-180' : ''}
|
||||
>
|
||||
<Icon icon={ChevronDown} size={4} color="text-gray-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
transition
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className={`ease-in-out ${
|
||||
isOpen ? 'max-h-[2000px] opacity-100' : 'max-h-0 opacity-0 overflow-hidden'
|
||||
}`}
|
||||
>
|
||||
<Box px={4} pb={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
|
||||
<Stack px={4} pb={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1193,10 +1192,10 @@ export function LeagueTimingsSection({
|
||||
<Stack gap={4}>
|
||||
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Schedule & timings</Heading>
|
||||
<Stack gap={3}>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text weight="medium" color="text-gray-200">Planned rounds:</Text>{' '}
|
||||
<Text color="text-gray-300">{timings.roundsPlanned ?? '—'}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
@@ -1213,7 +1212,7 @@ export function LeagueTimingsSection({
|
||||
];
|
||||
|
||||
return (
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, lg: 2 }} gap={6}>
|
||||
<Stack display="grid" responsiveGridCols={{ base: 1, lg: 2 }} gap={6}>
|
||||
{/* LEFT COLUMN: Configuration */}
|
||||
<Stack gap={4}>
|
||||
{/* Session Durations - Collapsible */}
|
||||
@@ -1223,7 +1222,7 @@ export function LeagueTimingsSection({
|
||||
description="Configure practice, qualifying, and race lengths"
|
||||
defaultOpen={false}
|
||||
>
|
||||
<Box display="grid" gridCols={2} gap={3}>
|
||||
<Stack display="grid" gridCols={2} gap={3}>
|
||||
<RangeField
|
||||
label="Practice"
|
||||
value={timings.practiceMinutes ?? 20}
|
||||
@@ -1264,7 +1263,7 @@ export function LeagueTimingsSection({
|
||||
compact
|
||||
error={errors?.mainRaceMinutes}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Season Length - Collapsible */}
|
||||
@@ -1301,7 +1300,7 @@ export function LeagueTimingsSection({
|
||||
{/* Frequency */}
|
||||
<Stack gap={2} mb={4}>
|
||||
<Text as="label" size="xs" color="text-gray-400" block>How often?</Text>
|
||||
<Box display="flex" gap={2}>
|
||||
<Stack display="flex" gap={2}>
|
||||
{[
|
||||
{ id: 'weekly', label: 'Weekly' },
|
||||
{ id: 'everyNWeeks', label: 'Every 2 weeks' },
|
||||
@@ -1310,7 +1309,7 @@ export function LeagueTimingsSection({
|
||||
(opt.id === 'weekly' && recurrenceStrategy === 'weekly') ||
|
||||
(opt.id === 'everyNWeeks' && recurrenceStrategy === 'everyNWeeks');
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={opt.id}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -1336,15 +1335,15 @@ export function LeagueTimingsSection({
|
||||
style={{ fontSize: '12px', fontWeight: 500 }}
|
||||
>
|
||||
{opt.label}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Day selection */}
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack display="flex" alignItems="center" justifyContent="between">
|
||||
<Text as="label" size="xs" color="text-gray-400">Which days?</Text>
|
||||
{weekdays.length === 0 && (
|
||||
<Text
|
||||
@@ -1355,13 +1354,13 @@ export function LeagueTimingsSection({
|
||||
Select at least one
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box display="flex" gap={1}>
|
||||
<Stack display="flex" gap={1}>
|
||||
{allWeekdays.map(({ day, short }) => {
|
||||
const isSelected = weekdays.includes(day);
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={day}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -1380,10 +1379,10 @@ export function LeagueTimingsSection({
|
||||
style={{ fontSize: '10px', fontWeight: 500 }}
|
||||
>
|
||||
{short}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSection>
|
||||
|
||||
@@ -1395,7 +1394,7 @@ export function LeagueTimingsSection({
|
||||
defaultOpen={false}
|
||||
>
|
||||
<Stack gap={4}>
|
||||
<Box display="grid" gridCols={2} gap={3}>
|
||||
<Stack display="grid" gridCols={2} gap={3}>
|
||||
<Stack gap={1.5}>
|
||||
<Text as="label"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -1406,7 +1405,7 @@ export function LeagueTimingsSection({
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-performance-green" />
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-performance-green" />
|
||||
Season Start
|
||||
</Text>
|
||||
<Input
|
||||
@@ -1427,7 +1426,7 @@ export function LeagueTimingsSection({
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
>
|
||||
<Box w="2" h="2" rounded="sm" bg="bg-warning-amber" />
|
||||
<Stack w="2" h="2" rounded="sm" bg="bg-warning-amber" />
|
||||
Season End
|
||||
</Text>
|
||||
<Input
|
||||
@@ -1438,10 +1437,10 @@ export function LeagueTimingsSection({
|
||||
className="bg-iron-gray/30"
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{timings.seasonStartDate && timings.seasonEndDate && (
|
||||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
|
||||
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
|
||||
<Icon icon={Info} size={3.5} color="text-primary-blue" mt={0.5} flexShrink={0} />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -1450,7 +1449,7 @@ export function LeagueTimingsSection({
|
||||
>
|
||||
Races will be <Text as="span" color="text-white" weight="medium">evenly distributed</Text> between start and end dates on your selected race days.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack gap={1.5}>
|
||||
@@ -1481,12 +1480,12 @@ export function LeagueTimingsSection({
|
||||
>
|
||||
Time Zone
|
||||
</Text>
|
||||
<Box display="grid" gridCols={2} gap={2}>
|
||||
<Stack display="grid" gridCols={2} gap={2}>
|
||||
{TIME_ZONES.slice(0, 2).map((tz) => {
|
||||
const isSelected = (timings.timezoneId ?? 'UTC') === tz.value;
|
||||
const TzIcon = tz.icon;
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={tz.value}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -1506,7 +1505,7 @@ export function LeagueTimingsSection({
|
||||
textAlign="left"
|
||||
>
|
||||
<Icon icon={TzIcon} size={4} color={isSelected ? (tz.value === 'track' ? 'text-performance-green' : 'text-primary-blue') : 'text-gray-400'} />
|
||||
<Box flexGrow={1}>
|
||||
<Stack flexGrow={1}>
|
||||
<Text size="xs" weight="medium" color={isSelected ? 'text-white' : 'text-gray-300'} block>
|
||||
{tz.label}
|
||||
</Text>
|
||||
@@ -1520,14 +1519,14 @@ export function LeagueTimingsSection({
|
||||
Adjusts per track
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* More time zones - expandable */}
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
@@ -1545,15 +1544,15 @@ export function LeagueTimingsSection({
|
||||
>
|
||||
{showAdvanced ? 'Hide' : 'Show'} more time zones
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{showAdvanced && (
|
||||
<Box display="grid" gridCols={2} gap={2} mt={2}>
|
||||
<Stack display="grid" gridCols={2} gap={2} mt={2}>
|
||||
{TIME_ZONES.slice(2).map((tz) => {
|
||||
const isSelected = (timings.timezoneId ?? 'UTC') === tz.value;
|
||||
const TzIcon = tz.icon;
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={tz.value}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -1573,14 +1572,14 @@ export function LeagueTimingsSection({
|
||||
>
|
||||
<Icon icon={TzIcon} size={3.5} color={isSelected ? 'text-primary-blue' : 'text-gray-500'} />
|
||||
<Text size="xs" color={isSelected ? 'text-white' : 'text-gray-400'}>{tz.label}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-performance-green/5" border borderColor="border-performance-green/20">
|
||||
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-performance-green/5" border borderColor="border-performance-green/20">
|
||||
<Icon icon={Info} size={3.5} color="text-performance-green" mt={0.5} flexShrink={0} />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -1592,35 +1591,35 @@ export function LeagueTimingsSection({
|
||||
: 'All race times will be displayed in the selected time zone for consistency.'
|
||||
}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSection>
|
||||
</Stack>
|
||||
|
||||
{/* RIGHT COLUMN: Live Preview */}
|
||||
<Stack gap={4}>
|
||||
<Box
|
||||
<Stack
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ position: 'sticky', top: '1rem' }}
|
||||
>
|
||||
<Box rounded="xl" border borderColor="border-charcoal-outline"
|
||||
<Stack rounded="xl" border borderColor="border-charcoal-outline"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="bg-gradient-to-br from-iron-gray/80 to-deep-graphite"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Preview header with tabs */}
|
||||
<Box display="flex" alignItems="center" justifyContent="between" p={3} borderBottom borderColor="border-charcoal-outline/50" bg="bg-deep-graphite/50">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" p={3} borderBottom borderColor="border-charcoal-outline/50" bg="bg-deep-graphite/50">
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Eye} size={4} color="text-primary-blue" />
|
||||
<Text size="xs" weight="semibold" color="text-white">Preview</Text>
|
||||
</Box>
|
||||
<Box display="flex" gap={1} p={0.5} rounded="lg" bg="bg-iron-gray/60">
|
||||
</Stack>
|
||||
<Stack display="flex" gap={1} p={0.5} rounded="lg" bg="bg-iron-gray/60">
|
||||
{[
|
||||
{ id: 'day', label: 'Race Day', icon: Play },
|
||||
{ id: 'year', label: 'Calendar', icon: CalendarRange },
|
||||
{ id: 'stats', label: 'Stats', icon: Trophy },
|
||||
].map((tab) => (
|
||||
<Box
|
||||
<Stack
|
||||
key={tab.id}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -1639,19 +1638,19 @@ export function LeagueTimingsSection({
|
||||
style={{ fontSize: '10px', fontWeight: 500 }}
|
||||
>
|
||||
<Icon icon={tab.icon} size={3} />
|
||||
<Box as="span"
|
||||
<Stack as="span"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="hidden sm:inline"
|
||||
>
|
||||
{tab.label}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Preview content */}
|
||||
<Box p={4}
|
||||
<Stack p={4}
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ minHeight: '300px' }}
|
||||
>
|
||||
@@ -1708,11 +1707,11 @@ export function LeagueTimingsSection({
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Helper tip */}
|
||||
<Box mt={3} display="flex" alignItems="start" gap={2} p={3} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
|
||||
<Stack mt={3} display="flex" alignItems="start" gap={2} p={3} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
|
||||
<Icon icon={Info} size={4} color="text-primary-blue" mt={0.5} flexShrink={0} />
|
||||
<Text
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
@@ -1722,9 +1721,9 @@ export function LeagueTimingsSection({
|
||||
>
|
||||
Preview updates live as you configure. Check <Text as="span" color="text-white" weight="medium">Race Day</Text> for session timing, <Text as="span" color="text-white" weight="medium">Calendar</Text> for the full year view, and <Text as="span" color="text-white" weight="medium">Stats</Text> for season totals.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,9 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import type * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
// Minimum drivers for ranked leagues
|
||||
@@ -87,7 +86,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
if (!isOpen || !mounted) return null;
|
||||
|
||||
return createPortal(
|
||||
<Box
|
||||
<Stack
|
||||
ref={flyoutRef}
|
||||
position="fixed"
|
||||
zIndex={50}
|
||||
@@ -100,7 +99,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
|
||||
>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
@@ -116,7 +115,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
|
||||
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
|
||||
</Stack>
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -130,12 +129,12 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
|
||||
hoverBg="bg-charcoal-outline"
|
||||
>
|
||||
<Icon icon={X} size={4} color="text-gray-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box p={4}>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack p={4}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>,
|
||||
</Stack>
|
||||
</Stack>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -189,20 +188,20 @@ export function LeagueVisibilitySection({
|
||||
return (
|
||||
<Stack gap={8}>
|
||||
{/* Emotional header for the step */}
|
||||
<Box textAlign="center" pb={2}>
|
||||
<Stack textAlign="center" pb={2}>
|
||||
<Heading level={3} mb={2}>
|
||||
Choose your league's destiny
|
||||
</Heading>
|
||||
<Text size="sm" color="text-gray-400" maxWidth="lg" mx="auto" block>
|
||||
Will you compete for glory on the global leaderboards, or race with friends in a private series?
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* League Type Selection */}
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
<Stack display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
{/* Ranked (Public) Option */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
<Stack position="relative">
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
@@ -227,9 +226,9 @@ export function LeagueVisibilitySection({
|
||||
group
|
||||
>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="14"
|
||||
w="14"
|
||||
@@ -239,18 +238,18 @@ export function LeagueVisibilitySection({
|
||||
bg={isRanked ? 'bg-primary-blue/30' : 'bg-charcoal-outline/50'}
|
||||
>
|
||||
<Icon icon={Trophy} size={7} color={isRanked ? 'text-primary-blue' : 'text-gray-400'} />
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text weight="bold" size="xl" color={isRanked ? 'text-white' : 'text-gray-300'} block>
|
||||
Ranked
|
||||
</Text>
|
||||
<Text size="sm" color={isRanked ? 'text-primary-blue' : 'text-gray-500'} block>
|
||||
Compete for glory
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{/* Radio indicator */}
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="7"
|
||||
w="7"
|
||||
@@ -264,8 +263,8 @@ export function LeagueVisibilitySection({
|
||||
transition
|
||||
>
|
||||
{isRanked && <Icon icon={Check} size={4} color="text-white" />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Emotional tagline */}
|
||||
<Text size="sm" color={isRanked ? 'text-gray-300' : 'text-gray-500'} block>
|
||||
@@ -289,16 +288,16 @@ export function LeagueVisibilitySection({
|
||||
</Stack>
|
||||
|
||||
{/* Requirement badge */}
|
||||
<Box display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20" w="fit">
|
||||
<Stack display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20" w="fit">
|
||||
<Icon icon={Users} size={4} color="text-warning-amber" />
|
||||
<Text size="xs" color="text-warning-amber" weight="medium">
|
||||
Requires {MIN_RANKED_DRIVERS}+ drivers for competitive integrity
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Info button */}
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
ref={rankedInfoRef}
|
||||
type="button"
|
||||
@@ -318,8 +317,8 @@ export function LeagueVisibilitySection({
|
||||
hoverBg="bg-primary-blue/10"
|
||||
>
|
||||
<Icon icon={HelpCircle} size={4} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Ranked Info Flyout */}
|
||||
<InfoFlyout
|
||||
@@ -343,16 +342,16 @@ export function LeagueVisibilitySection({
|
||||
Requirements
|
||||
</Text>
|
||||
<Stack gap={1.5}>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Users} size={3.5} color="text-warning-amber" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">
|
||||
<Text weight="bold" color="text-white">Minimum {MIN_RANKED_DRIVERS} drivers</Text> for competitive integrity
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3.5} color="text-performance-green" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Anyone can discover and join your league</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -365,22 +364,22 @@ export function LeagueVisibilitySection({
|
||||
Benefits
|
||||
</Text>
|
||||
<Stack gap={1.5}>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Trophy} size={3.5} color="text-primary-blue" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Results affect driver ratings and rankings</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3.5} color="text-performance-green" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Featured in league discovery</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</InfoFlyout>
|
||||
|
||||
{/* Unranked (Private) Option */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
<Stack position="relative">
|
||||
<Stack
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
@@ -405,9 +404,9 @@ export function LeagueVisibilitySection({
|
||||
group
|
||||
>
|
||||
{/* Header */}
|
||||
<Box display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack display="flex" alignItems="start" justifyContent="between">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="14"
|
||||
w="14"
|
||||
@@ -417,18 +416,18 @@ export function LeagueVisibilitySection({
|
||||
bg={!isRanked ? 'bg-neon-aqua/30' : 'bg-charcoal-outline/50'}
|
||||
>
|
||||
<Icon icon={Users} size={7} color={!isRanked ? 'text-neon-aqua' : 'text-gray-400'} />
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text weight="bold" size="xl" color={!isRanked ? 'text-white' : 'text-gray-300'} block>
|
||||
Unranked
|
||||
</Text>
|
||||
<Text size="sm" color={!isRanked ? 'text-neon-aqua' : 'text-gray-500'} block>
|
||||
Race with friends
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{/* Radio indicator */}
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
h="7"
|
||||
w="7"
|
||||
@@ -442,8 +441,8 @@ export function LeagueVisibilitySection({
|
||||
transition
|
||||
>
|
||||
{!isRanked && <Icon icon={Check} size={4} color="text-deep-graphite" />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Emotional tagline */}
|
||||
<Text size="sm" color={!isRanked ? 'text-gray-300' : 'text-gray-500'} block>
|
||||
@@ -467,16 +466,16 @@ export function LeagueVisibilitySection({
|
||||
</Stack>
|
||||
|
||||
{/* Flexibility badge */}
|
||||
<Box display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-neon-aqua/10" border borderColor="border-neon-aqua/20" w="fit">
|
||||
<Stack display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-neon-aqua/10" border borderColor="border-neon-aqua/20" w="fit">
|
||||
<Icon icon={Users} size={4} color={!isRanked ? 'text-neon-aqua' : 'text-gray-400'} />
|
||||
<Text size="xs" color={!isRanked ? 'text-neon-aqua' : 'text-gray-400'} weight="medium">
|
||||
Any size — even 2 friends
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Info button */}
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
ref={unrankedInfoRef}
|
||||
type="button"
|
||||
@@ -496,8 +495,8 @@ export function LeagueVisibilitySection({
|
||||
hoverBg="bg-neon-aqua/10"
|
||||
>
|
||||
<Icon icon={HelpCircle} size={4} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Unranked Info Flyout */}
|
||||
<InfoFlyout
|
||||
@@ -522,18 +521,18 @@ export function LeagueVisibilitySection({
|
||||
Perfect For
|
||||
</Text>
|
||||
<Stack gap={1.5}>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Private racing with friends</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Practice and training sessions</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Small groups (2+ drivers)</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -546,29 +545,29 @@ export function LeagueVisibilitySection({
|
||||
Features
|
||||
</Text>
|
||||
<Stack gap={1.5}>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Users} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Invite-only membership</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="start" gap={2}>
|
||||
</Stack>
|
||||
<Stack display="flex" alignItems="start" gap={2}>
|
||||
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
|
||||
<Text size="xs" color="text-gray-400">Full stats and standings (internal only)</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</InfoFlyout>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{errors?.visibility && (
|
||||
<Box display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
|
||||
<Stack display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
|
||||
<Icon icon={HelpCircle} size={4} color="text-warning-amber" flexShrink={0} />
|
||||
<Text size="xs" color="text-warning-amber">{errors.visibility}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Contextual info based on selection */}
|
||||
<Box
|
||||
<Stack
|
||||
rounded="xl"
|
||||
p={5}
|
||||
border
|
||||
@@ -576,33 +575,33 @@ export function LeagueVisibilitySection({
|
||||
bg={isRanked ? 'bg-primary-blue/5' : 'bg-neon-aqua/5'}
|
||||
borderColor={isRanked ? 'border-primary-blue/20' : 'border-neon-aqua/20'}
|
||||
>
|
||||
<Box display="flex" alignItems="start" gap={3}>
|
||||
<Stack display="flex" alignItems="start" gap={3}>
|
||||
{isRanked ? (
|
||||
<>
|
||||
<Icon icon={Trophy} size={5} color="text-primary-blue" flexShrink={0} mt={0.5} />
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="sm" weight="medium" color="text-white" block mb={1}>Ready to compete</Text>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
Your league will be visible to all GridPilot drivers. Results will affect driver ratings
|
||||
and contribute to the global leaderboards. Make sure you have at least {MIN_RANKED_DRIVERS} drivers
|
||||
to ensure competitive integrity.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon icon={Users} size={5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text size="sm" weight="medium" color="text-white" block mb={1}>Private racing awaits</Text>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
Your league will be invite-only. Perfect for racing with friends, practice sessions,
|
||||
or any time you want to have fun without affecting your official ratings.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
|
||||
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
||||
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
||||
import { Card } from "@/ui/Card";
|
||||
import { Box } from "@/ui/Box";
|
||||
import { Stack } from "@/ui/Stack";
|
||||
import { Text } from "@/ui/Text";
|
||||
import { Heading } from "@/ui/Heading";
|
||||
@@ -45,15 +44,15 @@ export function PenaltyHistoryList({
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
{filteredProtests.length === 0 ? (
|
||||
<Card py={12} textAlign="center">
|
||||
<Stack alignItems="center" gap={4}>
|
||||
<Card py={12} className="text-center">
|
||||
<Stack align="center" gap={4}>
|
||||
<Icon icon={AlertCircle} size={12} color="text-gray-400" opacity={0.5} />
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text weight="medium" size="lg" color="text-gray-400" block>No Resolved Protests</Text>
|
||||
<Text size="sm" color="text-gray-500" mt={1} block>
|
||||
No protests have been resolved in this league
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -68,45 +67,42 @@ export function PenaltyHistoryList({
|
||||
|
||||
return (
|
||||
<Card key={protest.id} p={4}>
|
||||
<Box display="flex" alignItems="start" gap={4}>
|
||||
<Box
|
||||
<Stack direction="row" align="start" gap={4}>
|
||||
<Stack
|
||||
w="10"
|
||||
h="10"
|
||||
rounded="full"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
align="center"
|
||||
justify="center"
|
||||
flexShrink={0}
|
||||
bg={statusColors.bg}
|
||||
color={statusColors.text}
|
||||
>
|
||||
<Icon icon={Flag} size={5} />
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
</Stack>
|
||||
<Stack flex={1}>
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" alignItems="start" justifyContent="between" gap={4}>
|
||||
<Box>
|
||||
<Stack direction="row" align="start" justify="between" gap={4}>
|
||||
<Stack>
|
||||
<Heading level={3}>
|
||||
Protest #{protest.id.substring(0, 8)}
|
||||
</Heading>
|
||||
<Text size="sm" color="text-gray-400" block>
|
||||
{resolvedDate ? `Resolved ${new Date(resolvedDate).toLocaleDateString()}` : 'Resolved'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
px={3}
|
||||
py={1}
|
||||
rounded="full"
|
||||
bg={statusColors.bg}
|
||||
color={statusColors.text}
|
||||
fontSize="12px"
|
||||
weight="medium"
|
||||
flexShrink={0}
|
||||
className="text-[12px] font-medium flex-shrink-0"
|
||||
>
|
||||
{protest.status.toUpperCase()}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" color="text-gray-400" block>
|
||||
<Text weight="medium" color="text-white">{protester?.name || 'Unknown'}</Text> vs <Text weight="medium" color="text-white">{accused?.name || 'Unknown'}</Text>
|
||||
</Text>
|
||||
@@ -115,20 +111,20 @@ export function PenaltyHistoryList({
|
||||
{race.track} ({race.car}) - Lap {incident.lap}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
{incident && (
|
||||
<Text size="sm" color="text-gray-300" block>{incident.description}</Text>
|
||||
)}
|
||||
{protest.decisionNotes && (
|
||||
<Box mt={2} p={2} rounded="md" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
|
||||
<Stack mt={2} p={2} rounded="md" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
<Text weight="medium">Steward Notes:</Text> {protest.decisionNotes}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
@@ -136,4 +132,4 @@ export function PenaltyHistoryList({
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
|
||||
|
||||
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
||||
import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
|
||||
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
||||
import { Box } from "@/ui/Box";
|
||||
import { Stack } from "@/ui/Stack";
|
||||
import { Card } from "@/ui/Card";
|
||||
import { ProtestListItem } from "./ProtestListItem";
|
||||
import { Stack } from "@/ui/Stack";
|
||||
import { Text } from "@/ui/Text";
|
||||
import { Flag } from "lucide-react";
|
||||
|
||||
@@ -28,17 +25,15 @@ export function PendingProtestsList({
|
||||
if (protests.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<Box p={12} textAlign="center">
|
||||
<Stack align="center" gap={4}>
|
||||
<Box w="16" h="16" rounded="full" bg="bg-performance-green/10" display="flex" alignItems="center" justifyContent="center">
|
||||
<Flag className="h-8 w-8 text-performance-green" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text weight="semibold" size="lg" color="text-white" block mb={2}>All Clear! 🏁</Text>
|
||||
<Text size="sm" color="text-gray-400">No pending protests to review</Text>
|
||||
</Box>
|
||||
<Stack p={12} align="center" gap={4}>
|
||||
<Stack w="16" h="16" rounded="full" bg="bg-performance-green/10" align="center" justify="center">
|
||||
<Flag className="h-8 w-8 text-performance-green" />
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack align="center">
|
||||
<Text weight="semibold" size="lg" color="text-white" block mb={2}>All Clear! 🏁</Text>
|
||||
<Text size="sm" color="text-gray-400">No pending protests to review</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import { AlertCircle, AlertTriangle, Video } from 'lucide-react';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface ProtestListItemProps {
|
||||
@@ -61,7 +60,7 @@ export function ProtestListItem({
|
||||
style={isUrgent ? { borderLeftWidth: '4px' } : undefined}
|
||||
>
|
||||
<Stack direction="row" align="start" justify="between" gap={4}>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Stack flexGrow={1} minWidth="0">
|
||||
<Stack direction="row" align="center" gap={2} mb={2} wrap>
|
||||
<Icon icon={AlertCircle} size={4} color="rgb(156, 163, 175)" />
|
||||
<Link href={protesterHref}>
|
||||
@@ -97,12 +96,12 @@ export function ProtestListItem({
|
||||
<Text size="sm" color="text-gray-300" block>{description}</Text>
|
||||
|
||||
{decisionNotes && (
|
||||
<Box mt={4} p={3} bg="bg-charcoal-outline/30" rounded="lg" border borderColor="border-charcoal-outline/50">
|
||||
<Stack mt={4} p={3} bg="bg-charcoal-outline/30" rounded="lg" border borderColor="border-charcoal-outline/50">
|
||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em" block mb={1}>Steward Decision</Text>
|
||||
<Text size="sm" color="text-gray-300">{decisionNotes}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
{isAdmin && status === 'pending' && onReview && (
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useState } from 'react';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { usePenaltyMutation } from "@/hooks/league/usePenaltyMutation";
|
||||
import { AlertTriangle, Clock, Flag, Zap } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Select } from '@/ui/Select';
|
||||
@@ -78,18 +77,18 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="fixed" inset="0" zIndex={50} display="flex" alignItems="center" justifyContent="center" p={4} bg="bg-black/70"
|
||||
<Stack position="fixed" inset="0" zIndex={50} display="flex" alignItems="center" justifyContent="center" p={4} bg="bg-black/70"
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className="backdrop-blur-sm"
|
||||
>
|
||||
<Box w="full" maxWidth="md" bg="bg-iron-gray" rounded="xl" border borderColor="border-charcoal-outline" shadow="2xl">
|
||||
<Box p={6}>
|
||||
<Stack w="full" maxWidth="md" bg="bg-iron-gray" rounded="xl" border borderColor="border-charcoal-outline" shadow="2xl">
|
||||
<Stack p={6}>
|
||||
<Heading level={2} fontSize="xl" weight="bold" color="text-white" mb={4}>Quick Penalty</Heading>
|
||||
|
||||
<Box as="form" onSubmit={handleSubmit} display="flex" flexDirection="col" gap={4}>
|
||||
<Stack as="form" onSubmit={handleSubmit} display="flex" flexDirection="col" gap={4}>
|
||||
{/* Race Selection */}
|
||||
{races && !raceId && (
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
Race
|
||||
</Text>
|
||||
@@ -105,18 +104,18 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Driver Selection */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
Driver
|
||||
</Text>
|
||||
{preSelectedDriver ? (
|
||||
<Box w="full" px={3} py={2} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="lg" color="text-white">
|
||||
<Stack w="full" px={3} py={2} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="lg" color="text-white">
|
||||
{preSelectedDriver.name}
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<Select
|
||||
value={selectedDriver}
|
||||
@@ -131,16 +130,16 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Infraction Type */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
Infraction Type
|
||||
</Text>
|
||||
<Box display="grid" gridCols={2} gap={2}>
|
||||
<Stack display="grid" gridCols={2} gap={2}>
|
||||
{INFRACTION_TYPES.map(({ value, label, icon: InfractionIcon }) => (
|
||||
<Box
|
||||
<Stack
|
||||
key={value}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -159,19 +158,19 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
|
||||
>
|
||||
<Icon icon={InfractionIcon} size={4} />
|
||||
<Text size="sm">{label}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Severity */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
Severity
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
{SEVERITY_LEVELS.map(({ value, label, description }) => (
|
||||
<Box
|
||||
<Stack
|
||||
key={value}
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -189,13 +188,13 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
|
||||
>
|
||||
<Text weight="medium" block>{label}</Text>
|
||||
<Text size="xs" opacity={0.75} block>{description}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Notes */}
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
Notes (Optional)
|
||||
</Text>
|
||||
@@ -205,16 +204,16 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
|
||||
placeholder="Additional details..."
|
||||
rows={3}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{error && (
|
||||
<Box p={3} bg="bg-red-500/10" border borderColor="border-red-500/20" rounded="lg">
|
||||
<Stack p={3} bg="bg-red-500/10" border borderColor="border-red-500/20" rounded="lg">
|
||||
<Text size="sm" color="text-red-400" block>{error}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Box display="flex" gap={3} pt={4}>
|
||||
<Stack display="flex" gap={3} pt={4}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -232,10 +231,10 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
|
||||
>
|
||||
{penaltyMutation.isPending ? 'Applying...' : 'Apply Penalty'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
|
||||
import { Modal } from "@/ui/Modal";
|
||||
import { Button } from "@/ui/Button";
|
||||
import { Card } from "@/ui/Card";
|
||||
import { Box } from "@/ui/Box";
|
||||
import { Text } from "@/ui/Text";
|
||||
import { Stack } from "@/ui/Stack";
|
||||
import { Text } from "@/ui/Text";
|
||||
import { Heading } from "@/ui/Heading";
|
||||
import { Icon } from "@/ui/Icon";
|
||||
import { TextArea } from "@/ui/TextArea";
|
||||
import { Input } from "@/ui/Input";
|
||||
import { Grid } from "@/ui/Grid";
|
||||
import {
|
||||
AlertCircle,
|
||||
Video,
|
||||
@@ -210,39 +210,39 @@ export function ReviewProtestModal({
|
||||
return (
|
||||
<Modal title="Confirm Decision" isOpen={true} onOpenChange={() => setShowConfirmation(false)}>
|
||||
<Stack gap={6} p={6}>
|
||||
<Box textAlign="center">
|
||||
<Stack align="center">
|
||||
<Stack gap={4}>
|
||||
{decision === "accept" ? (
|
||||
<Box display="flex" justifyContent="center">
|
||||
<Box h="16" w="16" rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center">
|
||||
<Stack direction="row" justify="center">
|
||||
<Stack h="16" w="16" rounded="full" bg="bg-orange-500/20" align="center" justify="center">
|
||||
<Icon icon={AlertCircle} size={8} color="text-orange-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Box display="flex" justifyContent="center">
|
||||
<Box h="16" w="16" rounded="full" bg="bg-gray-500/20" display="flex" alignItems="center" justifyContent="center">
|
||||
<Stack direction="row" justify="center">
|
||||
<Stack h="16" w="16" rounded="full" bg="bg-gray-500/20" align="center" justify="center">
|
||||
<Icon icon={XCircle} size={8} color="text-gray-400" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
<Box>
|
||||
<Heading level={3} fontSize="xl" weight="bold" color="text-white">Confirm Decision</Heading>
|
||||
<Text color="text-gray-400" mt={2} block>
|
||||
<Stack align="center">
|
||||
<Heading level={3} weight="bold" color="text-white">Confirm Decision</Heading>
|
||||
<Text color="text-gray-400" mt={2} block textAlign="center">
|
||||
{decision === "accept"
|
||||
? (selectedPenalty?.requiresValue
|
||||
? `Issue ${penaltyValue} ${selectedPenalty.valueLabel} penalty?`
|
||||
: `Issue ${selectedPenalty?.name ?? penaltyType} penalty?`)
|
||||
: "Reject this protest?"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Card p={4} className="bg-gray-800/50">
|
||||
<Text size="sm" color="text-gray-300" block>{stewardNotes}</Text>
|
||||
</Card>
|
||||
|
||||
<Box display="flex" gap={3}>
|
||||
<Stack direction="row" gap={3}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
@@ -259,7 +259,7 @@ export function ReviewProtestModal({
|
||||
>
|
||||
{submitting ? "Submitting..." : "Confirm Decision"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
@@ -268,94 +268,95 @@ export function ReviewProtestModal({
|
||||
return (
|
||||
<Modal title="Review Protest" isOpen={true} onOpenChange={onClose}>
|
||||
<Stack gap={6} p={6}>
|
||||
<Box display="flex" alignItems="start" gap={4}>
|
||||
<Box h="12" w="12" rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<Stack direction="row" align="start" gap={4}>
|
||||
<Stack h="12" w="12" rounded="full" bg="bg-orange-500/20" align="center" justify="center" flexShrink={0}>
|
||||
<Icon icon={AlertCircle} size={6} color="text-orange-400" />
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Heading level={2} fontSize="2xl" weight="bold" color="text-white">Review Protest</Heading>
|
||||
</Stack>
|
||||
<Stack flexGrow={1}>
|
||||
<Heading level={2} weight="bold" color="text-white">Review Protest</Heading>
|
||||
<Text color="text-gray-400" mt={1} block>
|
||||
Protest #{protest.id.substring(0, 8)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={4}>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Card p={4} className="bg-gray-800/50">
|
||||
<Stack gap={3}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text size="sm" color="text-gray-400">Filed Date</Text>
|
||||
<Text size="sm" color="text-white" weight="medium">
|
||||
{new Date(protest.filedAt || protest.submittedAt).toLocaleString()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text size="sm" color="text-gray-400">Incident Lap</Text>
|
||||
<Text size="sm" color="text-white" weight="medium">
|
||||
Lap {protest.incident?.lap || 'N/A'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text size="sm" color="text-gray-400">Status</Text>
|
||||
<Box as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
|
||||
<Stack as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
|
||||
<Text size="xs" weight="medium" color="text-orange-400">
|
||||
{protest.status}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Description
|
||||
</Text>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Card p={4} className="bg-gray-800/50">
|
||||
<Text color="text-gray-300" block>{protest.incident?.description || protest.description}</Text>
|
||||
</Card>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{protest.comment && (
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Additional Comment
|
||||
</Text>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Card p={4} className="bg-gray-800/50">
|
||||
<Text color="text-gray-300" block>{protest.comment}</Text>
|
||||
</Card>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{protest.proofVideoUrl && (
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Evidence
|
||||
</Text>
|
||||
<Card p={4} bg="bg-gray-800/50">
|
||||
<Box
|
||||
<Card p={4} className="bg-gray-800/50">
|
||||
<Stack
|
||||
as="a"
|
||||
href={protest.proofVideoUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
{...({
|
||||
href: protest.proofVideoUrl,
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer"
|
||||
} as any)}
|
||||
direction="row"
|
||||
align="center"
|
||||
gap={2}
|
||||
color="text-orange-400"
|
||||
hoverTextColor="text-orange-300"
|
||||
transition
|
||||
className="transition-all hover:text-orange-300"
|
||||
>
|
||||
<Icon icon={Video} size={4} />
|
||||
<Text size="sm">View video evidence</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Box borderTop borderColor="border-gray-800" pt={6}>
|
||||
<Stack borderTop borderColor="border-gray-800" pt={6}>
|
||||
<Stack gap={4}>
|
||||
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Stewarding Decision</Heading>
|
||||
<Heading level={3} weight="semibold" color="text-white">Stewarding Decision</Heading>
|
||||
|
||||
<Box display="grid" gridCols={2} gap={3}>
|
||||
<Grid cols={2} gap={3}>
|
||||
<Button
|
||||
variant={decision === "accept" ? "primary" : "secondary"}
|
||||
fullWidth
|
||||
@@ -376,11 +377,11 @@ export function ReviewProtestModal({
|
||||
Reject Protest
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{decision === "accept" && (
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Penalty Type
|
||||
</Text>
|
||||
@@ -388,11 +389,11 @@ export function ReviewProtestModal({
|
||||
{penaltyTypesLoading ? (
|
||||
<Text size="sm" color="text-gray-500">Loading penalty types…</Text>
|
||||
) : (
|
||||
<Box display="grid" gridCols={3} gap={2}>
|
||||
<Grid cols={3} gap={2}>
|
||||
{penaltyOptions.map(({ type, name, Icon: PenaltyIcon, colorClass, defaultValue }: { type: string; name: string; Icon: LucideIcon; colorClass: string; defaultValue: number }) => {
|
||||
const isSelected = penaltyType === type;
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
key={type}
|
||||
as="button"
|
||||
onClick={() => {
|
||||
@@ -402,25 +403,20 @@ export function ReviewProtestModal({
|
||||
p={3}
|
||||
rounded="lg"
|
||||
border
|
||||
borderWidth={isSelected ? "2px" : "1px"}
|
||||
transition
|
||||
borderColor={isSelected ? undefined : "border-charcoal-outline"}
|
||||
bg={isSelected ? undefined : "bg-iron-gray/50"}
|
||||
hoverBorderColor={!isSelected ? "border-gray-600" : undefined}
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
className={isSelected ? colorClass : ""}
|
||||
{...({ borderWidth: isSelected ? "2px" : "1px" } as any)}
|
||||
className={`transition-all ${isSelected ? colorClass : "bg-iron-gray/50 border-charcoal-outline hover:border-gray-600"}`}
|
||||
>
|
||||
<Icon icon={PenaltyIcon} size={5} mx="auto" mb={1} color={isSelected ? undefined : "text-gray-400"} />
|
||||
<Icon icon={PenaltyIcon} size={5} className="mx-auto mb-1" color={isSelected ? undefined : "text-gray-400"} />
|
||||
<Text size="xs" weight="medium" color={isSelected ? undefined : "text-gray-400"} block textAlign="center">{name}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{selectedPenalty?.requiresValue && (
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Penalty Value ({selectedPenalty.valueLabel})
|
||||
</Text>
|
||||
@@ -430,12 +426,12 @@ export function ReviewProtestModal({
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPenaltyValue(Number(e.target.value))}
|
||||
min={1}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Steward Notes *
|
||||
</Text>
|
||||
@@ -445,11 +441,11 @@ export function ReviewProtestModal({
|
||||
placeholder="Explain your decision and reasoning..."
|
||||
rows={4}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box display="flex" gap={3} pt={4} borderTop borderColor="border-gray-800">
|
||||
<Stack direction="row" gap={3} pt={4} borderTop borderColor="border-gray-800">
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
@@ -466,7 +462,7 @@ export function ReviewProtestModal({
|
||||
>
|
||||
Submit Decision
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
|
||||
@@ -11,7 +10,7 @@ interface RosterTableProps {
|
||||
|
||||
export function RosterTable({ children, columns = ['Driver', 'Role', 'Joined', 'Rating', 'Rank'] }: RosterTableProps) {
|
||||
return (
|
||||
<Box border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
|
||||
<Stack border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
|
||||
<Table>
|
||||
<TableHead className="bg-base-graphite/50">
|
||||
<TableRow>
|
||||
@@ -31,7 +30,7 @@ export function RosterTable({ children, columns = ['Driver', 'Role', 'Joined', '
|
||||
{children}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React from 'react';
|
||||
import { Calendar, Clock, MapPin, Car, Trophy } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -31,7 +30,7 @@ export function ScheduleRaceCard({ race }: ScheduleRaceCardProps) {
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
|
||||
<Stack w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
|
||||
<Heading level={3} fontSize="lg">{race.name}</Heading>
|
||||
<Badge variant={race.status === 'completed' ? 'success' : 'primary'}>
|
||||
{race.status === 'completed' ? 'Completed' : 'Scheduled'}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
@@ -97,7 +96,7 @@ function StatusBadge({ status }: { status: RaceEntry['status'] }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
as="span"
|
||||
px={2}
|
||||
py={0.5}
|
||||
@@ -113,6 +112,6 @@ function StatusBadge({ status }: { status: RaceEntry['status'] }) {
|
||||
display="inline-block"
|
||||
>
|
||||
{status.toUpperCase()}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
@@ -54,7 +53,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
|
||||
>
|
||||
<Stack direction="row" align="start" justify="between">
|
||||
{/* eslint-disable-next-line gridpilot-rules/component-classification */}
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack direction="row" align="center" gap={3} mb={2}>
|
||||
<Icon icon={statusIcon} size={5} color={statusColor} />
|
||||
<Text weight="semibold" color="text-white">{request.sponsorName}</Text>
|
||||
@@ -70,7 +69,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
{new Date(request.requestedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { DollarSign } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -58,10 +57,10 @@ export function SponsorshipSlotCard({ slot }: SponsorshipSlotCardProps) {
|
||||
|
||||
{!slot.isAvailable && slot.sponsoredBy && (
|
||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||||
<Box pt={3} style={{ borderTop: '1px solid #262626' }}>
|
||||
<Stack pt={3} style={{ borderTop: '1px solid #262626' }}>
|
||||
<Text size="xs" color="text-gray-400" block mb={1}>Sponsored by</Text>
|
||||
<Text size="sm" weight="medium" color="text-white">{slot.sponsoredBy.name}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Surface>
|
||||
|
||||
@@ -5,11 +5,10 @@ import { Link } from '@/ui/Link';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { CountryFlag } from '@/ui/CountryFlag';
|
||||
import { PlaceholderImage } from '@/ui/PlaceholderImage';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { User, Edit } from 'lucide-react';
|
||||
@@ -172,7 +171,7 @@ export function StandingsTable({
|
||||
const hasMembership = !!membership;
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
ref={menuRef}
|
||||
position="absolute"
|
||||
right="0"
|
||||
@@ -196,7 +195,7 @@ export function StandingsTable({
|
||||
<>
|
||||
{/* Role Management for existing members */}
|
||||
{membership!.role !== 'admin' && membership!.role !== 'owner' && (
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'admin'); }}
|
||||
display="flex"
|
||||
@@ -214,10 +213,10 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>🛡️</Text>
|
||||
<Text>Promote to Admin</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{membership!.role === 'admin' && (
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
|
||||
display="flex"
|
||||
@@ -235,10 +234,10 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>⬇️</Text>
|
||||
<Text>Demote to Member</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{membership!.role === 'member' && (
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'steward'); }}
|
||||
display="flex"
|
||||
@@ -256,10 +255,10 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>🏁</Text>
|
||||
<Text>Make Steward</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
{membership!.role === 'steward' && (
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
|
||||
display="flex"
|
||||
@@ -277,10 +276,10 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>🏁</Text>
|
||||
<Text>Remove Steward</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
<Box borderTop borderColor="border-charcoal-outline" my={1} />
|
||||
<Box
|
||||
<Stack borderTop borderColor="border-charcoal-outline" my={1} />
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRemove(driverId); }}
|
||||
display="flex"
|
||||
@@ -298,15 +297,15 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>🚫</Text>
|
||||
<Text>Remove from League</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Options for drivers without membership (participating but not formal members) */}
|
||||
<Box bg="bg-yellow-500/10" rounded px={2} py={1} mb={1}>
|
||||
<Stack bg="bg-yellow-500/10" rounded px={2} py={1} mb={1}>
|
||||
<Text size="xs" color="text-yellow-400/80">Driver not a formal member</Text>
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Add as member - feature coming soon'); }}
|
||||
display="flex"
|
||||
@@ -324,8 +323,8 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>➕</Text>
|
||||
<Text>Add as Member</Text>
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRemove(driverId); }}
|
||||
display="flex"
|
||||
@@ -343,17 +342,17 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>🚫</Text>
|
||||
<Text>Remove from Standings</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const PointsActionMenu = () => {
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
ref={menuRef}
|
||||
position="absolute"
|
||||
right="0"
|
||||
@@ -373,7 +372,7 @@ export function StandingsTable({
|
||||
Score Actions
|
||||
</Text>
|
||||
<Stack gap={1}>
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('View detailed stats - feature coming soon'); }}
|
||||
display="flex"
|
||||
@@ -391,8 +390,8 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>📊</Text>
|
||||
<Text>View Details</Text>
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Manual adjustment - feature coming soon'); }}
|
||||
display="flex"
|
||||
@@ -410,8 +409,8 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>⚠️</Text>
|
||||
<Text>Adjust Points</Text>
|
||||
</Box>
|
||||
<Box
|
||||
</Stack>
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Race history - feature coming soon'); }}
|
||||
display="flex"
|
||||
@@ -429,22 +428,22 @@ export function StandingsTable({
|
||||
>
|
||||
<Text>📝</Text>
|
||||
<Text>Race History</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
if (standings.length === 0) {
|
||||
return (
|
||||
<Box textAlign="center" py={8}>
|
||||
<Stack textAlign="center" py={8}>
|
||||
<Text color="text-gray-400">No standings available</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box overflow="auto">
|
||||
<Stack overflow="auto">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@@ -489,7 +488,7 @@ export function StandingsTable({
|
||||
>
|
||||
{/* Position */}
|
||||
<TableCell textAlign="center" w="14">
|
||||
<Box
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
@@ -510,15 +509,15 @@ export function StandingsTable({
|
||||
}
|
||||
>
|
||||
{row.position}
|
||||
</Box>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
{/* Driver with Rating and Nationality */}
|
||||
<TableCell position="relative">
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
{/* Avatar */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
<Stack position="relative">
|
||||
<Stack
|
||||
w="10"
|
||||
h="10"
|
||||
rounded="full"
|
||||
@@ -544,18 +543,18 @@ export function StandingsTable({
|
||||
<PlaceholderImage size={40} />
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
{/* Nationality flag */}
|
||||
{driver && driver.country && (
|
||||
<Box position="absolute" bottom="-1" right="-1">
|
||||
<Stack position="absolute" bottom="-1" right="-1">
|
||||
<CountryFlag countryCode={driver.country} size="sm" />
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Name and Rating */}
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack flexGrow={1} minWidth="0">
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Link
|
||||
href={routes.driver.detail(row.driverId)}
|
||||
weight="medium"
|
||||
@@ -570,7 +569,7 @@ export function StandingsTable({
|
||||
<Badge variant="primary">You</Badge>
|
||||
)}
|
||||
{roleDisplay && roleDisplay.text !== 'Member' && (
|
||||
<Box
|
||||
<Stack
|
||||
as="span"
|
||||
px={2}
|
||||
py={0.5}
|
||||
@@ -583,14 +582,14 @@ export function StandingsTable({
|
||||
borderColor={roleDisplay.borderColor}
|
||||
>
|
||||
{roleDisplay.text}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Hover Actions for Member Management */}
|
||||
{isAdmin && canModify && (
|
||||
<Box
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
@@ -598,7 +597,7 @@ export function StandingsTable({
|
||||
transition
|
||||
visibility={isRowHovered || isMemberMenuOpen ? 'visible' : 'hidden'}
|
||||
>
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setActiveMenu(isMemberMenuOpen ? null : { driverId: row.driverId, type: 'member' }); }}
|
||||
p={1.5}
|
||||
@@ -611,10 +610,10 @@ export function StandingsTable({
|
||||
title="Manage member"
|
||||
>
|
||||
<Icon icon={User} size={4} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
{isMemberMenuOpen && <MemberActionMenu driverId={row.driverId} />}
|
||||
</TableCell>
|
||||
|
||||
@@ -625,10 +624,10 @@ export function StandingsTable({
|
||||
|
||||
{/* Total Points with Hover Action */}
|
||||
<TableCell textAlign="right" position="relative">
|
||||
<Box display="flex" alignItems="center" justifyContent="end" gap={2}>
|
||||
<Stack display="flex" alignItems="center" justifyContent="end" gap={2}>
|
||||
<Text color="text-white" weight="bold" size="lg">{row.totalPoints}</Text>
|
||||
{isAdmin && canModify && (
|
||||
<Box
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setActiveMenu(isPointsMenuOpen ? null : { driverId: row.driverId, type: 'points' }); }}
|
||||
p={1}
|
||||
@@ -643,9 +642,9 @@ export function StandingsTable({
|
||||
title="Score actions"
|
||||
>
|
||||
<Icon icon={Edit} size={3} />
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
{isPointsMenuOpen && <PointsActionMenu />}
|
||||
</TableCell>
|
||||
|
||||
@@ -680,6 +679,6 @@ export function StandingsTable({
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
@@ -26,7 +25,7 @@ interface StandingsTableShellProps {
|
||||
export function StandingsTableShell({ standings, title = 'Championship Standings' }: StandingsTableShellProps) {
|
||||
return (
|
||||
<Surface variant="dark" border rounded="lg" overflow="hidden">
|
||||
<Box px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<Stack px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Trophy} size={4} color="text-warning-amber" />
|
||||
@@ -34,11 +33,11 @@ export function StandingsTableShell({ standings, title = 'Championship Standings
|
||||
{title.toUpperCase()}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Box px={2} py={0.5} rounded="md" bg="bg-charcoal-outline/50">
|
||||
<Stack px={2} py={0.5} rounded="md" bg="bg-charcoal-outline/50">
|
||||
<Text size="xs" color="text-gray-400" weight="medium">{standings.length} Drivers</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Table>
|
||||
<TableHead>
|
||||
@@ -100,7 +99,7 @@ function PositionBadge({ position }: { position: number }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Stack
|
||||
center
|
||||
w={8}
|
||||
h={8}
|
||||
@@ -113,6 +112,6 @@ function PositionBadge({ position }: { position: number }) {
|
||||
<Text size="sm" weight="bold">
|
||||
{position}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Clock, ShieldAlert, MessageSquare } from 'lucide-react';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Card } from '@/ui/Card';
|
||||
|
||||
interface Protest {
|
||||
id: string;
|
||||
@@ -26,34 +25,34 @@ interface StewardingQueuePanelProps {
|
||||
|
||||
export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePanelProps) {
|
||||
return (
|
||||
<Surface variant="dark" border rounded="lg" overflow="hidden">
|
||||
<Box px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<Card variant="outline" p={0} rounded="lg" overflow="hidden" className="bg-graphite-black">
|
||||
<Stack px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={ShieldAlert} size={4} color="text-error-red" />
|
||||
<Text weight="bold" letterSpacing="wider" size="sm" display="block">
|
||||
<Text weight="bold" letterSpacing="wider" size="sm" block>
|
||||
STEWARDING QUEUE
|
||||
</Text>
|
||||
</Stack>
|
||||
<Box px={2} py={0.5} rounded="md" bg="bg-error-red/10" border borderColor="border-error-red/20">
|
||||
<Stack px={2} py={0.5} rounded="md" bg="bg-error-red/10" border borderColor="border-error-red/20">
|
||||
<Text size="xs" color="text-error-red" weight="bold">
|
||||
{protests.filter(p => p.status === 'pending').length} Pending
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={0}>
|
||||
{protests.length === 0 ? (
|
||||
<Box py={12} center>
|
||||
<Stack py={12} align="center" justify="center">
|
||||
<Stack align="center" gap={3}>
|
||||
<Icon icon={ShieldAlert} size={8} color="text-gray-700" />
|
||||
<Text color="text-gray-500">No active protests in the queue.</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
protests.map((protest) => (
|
||||
<Box key={protest.id} p={6} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
||||
<Stack key={protest.id} p={6} borderBottom borderColor="border-charcoal-outline" className="transition-colors hover:bg-white/5">
|
||||
<Stack direction={{ base: 'col', md: 'row' }} justify="between" align="start" gap={4}>
|
||||
<Stack gap={3} flexGrow={1}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
@@ -61,7 +60,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
|
||||
<Text size="xs" color="text-gray-500" weight="bold" letterSpacing="widest">
|
||||
{protest.raceName.toUpperCase()}
|
||||
</Text>
|
||||
<Box w={1} h={1} rounded="full" bg="bg-gray-700" />
|
||||
<Stack w="1" h="1" rounded="full" bg="bg-gray-700">{null}</Stack>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Icon icon={Clock} size={3} color="text-gray-600" />
|
||||
<Text size="xs" color="text-gray-500">
|
||||
@@ -70,7 +69,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack>
|
||||
<Stack direction="row" align="center" gap={2} wrap>
|
||||
<Text weight="bold" color="text-white">{protest.protestingDriver}</Text>
|
||||
<Text size="xs" color="text-gray-600" weight="bold">VS</Text>
|
||||
@@ -79,7 +78,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
|
||||
<Text size="sm" color="text-gray-400" mt={2} lineClamp={2}>
|
||||
“{protest.description}”
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" align="center" gap={3} w={{ base: 'full', md: 'auto' }}>
|
||||
@@ -95,11 +94,11 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Surface>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,6 +111,6 @@ function StatusIndicator({ status }: { status: Protest['status'] }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box w={2} h={2} rounded="full" bg={colors[status]} animate={status === 'under_review' ? 'pulse' : 'none'} />
|
||||
<Stack w="2" h="2" rounded="full" bg={colors[status]} {...({ animate: status === 'under_review' ? 'pulse' : 'none' } as any)}>{null}</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { CheckCircle, Clock, Gavel } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { StatBox } from '@/ui/StatBox';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
|
||||
interface StewardingStatsProps {
|
||||
totalPending: number;
|
||||
@@ -11,7 +11,7 @@ interface StewardingStatsProps {
|
||||
|
||||
export function StewardingStats({ totalPending, totalResolved, totalPenalties }: StewardingStatsProps) {
|
||||
return (
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, sm: 3 }} gap={4} mb={6}>
|
||||
<Grid cols={1} mdCols={3} gap={4} mb={6}>
|
||||
<StatBox
|
||||
icon={Clock}
|
||||
label="Pending Review"
|
||||
@@ -30,6 +30,6 @@ export function StewardingStats({ totalPending, totalResolved, totalPenalties }:
|
||||
value={totalPenalties}
|
||||
color="var(--racing-red)"
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import { ArrowUpRight, ArrowDownRight, DollarSign, TrendingUp, LucideIcon } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
@@ -46,10 +45,10 @@ export function TransactionRow({ transaction }: TransactionRowProps) {
|
||||
>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box flexShrink={0}>
|
||||
<Stack flexShrink={0}>
|
||||
<Icon icon={getTransactionIcon(transaction.type)} size={4} color={transaction.typeColor} />
|
||||
</Box>
|
||||
<Box minWidth="0" flexGrow={1}>
|
||||
</Stack>
|
||||
<Stack minWidth="0" flexGrow={1}>
|
||||
<Text size="sm" weight="medium" color="text-white" block truncate>
|
||||
{transaction.description}
|
||||
</Text>
|
||||
@@ -64,14 +63,14 @@ export function TransactionRow({ transaction }: TransactionRowProps) {
|
||||
{transaction.status}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box textAlign="right">
|
||||
<Stack textAlign="right">
|
||||
<Text size="lg" weight="semibold" color={transaction.amountColor}>
|
||||
{transaction.formattedAmount}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
@@ -30,7 +29,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
||||
<Stack gap={6}>
|
||||
<Surface variant="dark" border rounded="lg" padding={8} position="relative" overflow="hidden">
|
||||
{/* Background Pattern */}
|
||||
<Box
|
||||
<Stack
|
||||
position="absolute"
|
||||
top="-5rem"
|
||||
right="-5rem"
|
||||
@@ -74,26 +73,26 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
||||
</Surface>
|
||||
|
||||
<Surface variant="dark" border rounded="lg" overflow="hidden">
|
||||
<Box px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<Stack px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={History} size={4} color="text-primary-blue" />
|
||||
<Text weight="bold" letterSpacing="wider" size="sm" display="block">
|
||||
RECENT TRANSACTIONS
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={0}>
|
||||
{transactions.length === 0 ? (
|
||||
<Box py={12} center>
|
||||
<Stack py={12} center>
|
||||
<Text color="text-gray-500">No recent transactions.</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
transactions.map((tx) => (
|
||||
<Box key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
||||
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Box
|
||||
<Stack
|
||||
center
|
||||
w={10}
|
||||
h={10}
|
||||
@@ -105,7 +104,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
||||
size={4}
|
||||
color={tx.type === 'credit' ? 'text-performance-green' : 'text-error-red'}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack gap={0.5}>
|
||||
<Text weight="medium" color="text-white">{tx.description}</Text>
|
||||
<Text size="xs" color="text-gray-500">{new Date(tx.date).toLocaleDateString()}</Text>
|
||||
@@ -118,7 +117,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
||||
{tx.type === 'credit' ? '+' : '-'}{tx.amount.toFixed(2)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user