website refactor
This commit is contained in:
@@ -6,7 +6,7 @@ import { AdminUsersTemplate } from '@/templates/AdminUsersTemplate';
|
||||
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
|
||||
import { updateUserStatus, deleteUser } from '@/app/actions/adminActions';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { SharedConfirmDialog } from '@/components/shared/UIComponents';
|
||||
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
|
||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
|
||||
export function AdminUsersWrapper({ viewData }: ClientWrapperProps<AdminUsersViewData>) {
|
||||
@@ -148,7 +148,7 @@ export function AdminUsersWrapper({ viewData }: ClientWrapperProps<AdminUsersVie
|
||||
onSelectAll={handleSelectAll}
|
||||
onClearSelection={handleClearSelection}
|
||||
/>
|
||||
<SharedConfirmDialog
|
||||
<ConfirmDialog
|
||||
isOpen={!!userToDelete}
|
||||
onClose={() => setUserToDelete(null)}
|
||||
onConfirm={confirmDeleteUser}
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
updateRaceAction
|
||||
} from '@/app/actions/leagueScheduleActions';
|
||||
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
|
||||
import { SharedConfirmDialog, SharedStack, SharedCard, SharedBox, SharedText, SharedHeading } from '@/components/shared/UIComponents';
|
||||
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import {
|
||||
useLeagueAdminSchedule,
|
||||
useLeagueAdminStatus,
|
||||
@@ -172,16 +177,16 @@ export function LeagueAdminSchedulePageClient() {
|
||||
// Render admin access required if not admin
|
||||
if (!isLoading && !isAdmin) {
|
||||
return (
|
||||
<SharedStack gap={6}>
|
||||
<SharedCard>
|
||||
<SharedBox p={6} textAlign="center">
|
||||
<SharedHeading level={3}>Admin Access Required</SharedHeading>
|
||||
<SharedBox mt={2}>
|
||||
<SharedText size="sm" color="text-gray-400">Only league admins can manage the schedule.</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</SharedStack>
|
||||
<Stack gap={6}>
|
||||
<Card>
|
||||
<Box p={6} textAlign="center">
|
||||
<Heading level={3}>Admin Access Required</Heading>
|
||||
<Box mt={2}>
|
||||
<Text size="sm" color="text-gray-400">Only league admins can manage the schedule.</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,7 +226,7 @@ export function LeagueAdminSchedulePageClient() {
|
||||
setForm(new RaceScheduleCommandModel(form.toCommand()));
|
||||
}}
|
||||
/>
|
||||
<SharedConfirmDialog
|
||||
<ConfirmDialog
|
||||
isOpen={!!raceToDelete}
|
||||
onClose={() => setRaceToDelete(null)}
|
||||
onConfirm={confirmDelete}
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
import type { Result } from '@/lib/contracts/Result';
|
||||
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
|
||||
import { ProfileSettingsTemplate } from '@/templates/ProfileSettingsTemplate';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedIcon,
|
||||
SharedProgressLine
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { ProgressLine } from '@/components/shared/ProgressLine';
|
||||
import { ShieldAlert } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -46,19 +44,19 @@ export function ProfileSettingsPageClient({ viewData, onSave }: ProfileSettingsP
|
||||
|
||||
return (
|
||||
<>
|
||||
<SharedProgressLine isLoading={isSaving} />
|
||||
<ProgressLine isLoading={isSaving} />
|
||||
{error && (
|
||||
<SharedBox position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
|
||||
<SharedBox bg="bg-error-red/10" p={4} rounded="md" border borderColor="border-error-red/20">
|
||||
<SharedStack direction="row" align="center" gap={3}>
|
||||
<SharedIcon icon={ShieldAlert} size={5} color="text-error-red" />
|
||||
<SharedBox>
|
||||
<SharedText weight="bold" color="text-error-red">Update Failed</SharedText>
|
||||
<SharedText size="sm" color="text-error-red/80">{error}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
<Box position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
|
||||
<Box bg="var(--ui-color-bg-surface)" p={4} rounded="md" border borderColor="var(--ui-color-intent-critical)">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Icon icon={ShieldAlert} size={5} color="var(--ui-color-intent-critical)" />
|
||||
<Box>
|
||||
<Text weight="bold" variant="critical">Update Failed</Text>
|
||||
<Text size="sm" variant="low">{error}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<ProfileSettingsTemplate
|
||||
viewData={viewData}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
SharedBox,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedIcon,
|
||||
SharedProgressLine
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { ProgressLine } from '@/components/shared/ProgressLine';
|
||||
import { ShieldAlert } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -53,19 +51,19 @@ export function SponsorshipRequestsClient({ viewData, onAccept, onReject }: Spon
|
||||
|
||||
return (
|
||||
<>
|
||||
<SharedProgressLine isLoading={!!isProcessing} />
|
||||
<ProgressLine isLoading={!!isProcessing} />
|
||||
{error && (
|
||||
<SharedBox position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
|
||||
<SharedBox bg="bg-error-red/10" p={4} rounded="md" border borderColor="border-error-red/20">
|
||||
<SharedStack direction="row" align="center" gap={3}>
|
||||
<SharedIcon icon={ShieldAlert} size={5} color="text-error-red" />
|
||||
<SharedBox>
|
||||
<SharedText weight="bold" color="text-error-red">Action Failed</SharedText>
|
||||
<SharedText size="sm" color="text-error-red/80">{error}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
<Box position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
|
||||
<Box bg="bg-error-red/10" p={4} rounded="md" border borderColor="border-error-red/20">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Icon icon={ShieldAlert} size={5} color="text-error-red" />
|
||||
<Box>
|
||||
<Text weight="bold" color="text-error-red">Action Failed</Text>
|
||||
<Text size="sm" color="text-error-red/80">{error}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<SponsorshipRequestsTemplate
|
||||
viewData={viewData}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function AppFooter({ children }: AppFooterProps) {
|
||||
variant="muted"
|
||||
paddingY={8}
|
||||
marginTop="auto"
|
||||
style={{ borderTop: '1px solid var(--ui-color-border-default)' }}
|
||||
borderTop={true}
|
||||
>
|
||||
<Container size="xl">
|
||||
{children ? (
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { SharedBox, SharedStack, SharedContainer } from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Container } from '@/ui/Container';
|
||||
|
||||
interface CreateLeagueWizardLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -13,12 +15,12 @@ interface CreateLeagueWizardLayoutProps {
|
||||
|
||||
export function CreateLeagueWizardLayout({ children, header, progress, navigation, footer }: CreateLeagueWizardLayoutProps) {
|
||||
return (
|
||||
<SharedBox as="main" maxWidth="4xl" mx="auto" pb={8}>
|
||||
<Box as="main" maxWidth="4xl" mx="auto" pb={8}>
|
||||
{header}
|
||||
{progress}
|
||||
{children}
|
||||
{navigation}
|
||||
{footer}
|
||||
</SharedBox>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
|
||||
interface SharedEmptyStateProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'race-final' | 'discord';
|
||||
};
|
||||
}
|
||||
|
||||
export function SharedEmptyState({ icon, title, description, action }: SharedEmptyStateProps) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={icon}
|
||||
title={title}
|
||||
description={description}
|
||||
action={action}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Pagination } from '@/ui/Pagination';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { GridItem } from '@/ui/GridItem';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Skeleton } from '@/ui/Skeleton';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
|
||||
import { ProgressLine } from '@/components/shared/ProgressLine';
|
||||
import { SharedEmptyState } from './SharedEmptyState';
|
||||
|
||||
export {
|
||||
Pagination as SharedPagination,
|
||||
Text as SharedText,
|
||||
Box as SharedBox,
|
||||
Stack as SharedStack,
|
||||
Container as SharedContainer,
|
||||
ConfirmDialog as SharedConfirmDialog,
|
||||
Button as SharedButton,
|
||||
Icon as SharedIcon,
|
||||
Card as SharedCard,
|
||||
Heading as SharedHeading,
|
||||
Grid as SharedGrid,
|
||||
GridItem as SharedGridItem,
|
||||
Surface as SharedSurface,
|
||||
Input as SharedInput,
|
||||
Link as SharedLink,
|
||||
Skeleton as SharedSkeleton,
|
||||
LoadingSpinner as SharedLoadingSpinner,
|
||||
Badge as SharedBadge,
|
||||
ProgressLine as SharedProgressLine,
|
||||
SharedEmptyState
|
||||
};
|
||||
@@ -8,7 +8,13 @@ import { AdminUsersTable } from '@/components/admin/AdminUsersTable';
|
||||
import { BulkActionBar } from '@/components/admin/BulkActionBar';
|
||||
import { UserFilters } from '@/components/admin/UserFilters';
|
||||
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
|
||||
import { SharedButton, SharedContainer, SharedIcon, SharedStack, SharedBox, SharedText } from '@/components/shared/UIComponents';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ErrorBanner } from '@/ui/ErrorBanner';
|
||||
import { RefreshCw, ShieldAlert, Users } from 'lucide-react';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
|
||||
@@ -95,37 +101,29 @@ export function AdminUsersTemplate({
|
||||
];
|
||||
|
||||
return (
|
||||
<SharedContainer size="lg">
|
||||
<SharedBox paddingY={8}>
|
||||
<SharedStack gap={8}>
|
||||
<Container size="lg">
|
||||
<Box paddingY={8}>
|
||||
<Stack gap={8}>
|
||||
<AdminHeaderPanel
|
||||
title="User Management"
|
||||
description="Monitor and control system access"
|
||||
isLoading={loading}
|
||||
actions={
|
||||
<SharedButton
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={<SharedIcon icon={RefreshCw} size={3} animate={loading ? 'spin' : 'none'} />}
|
||||
icon={<Icon icon={RefreshCw} size={3} animate={loading ? 'spin' : 'none'} />}
|
||||
>
|
||||
Refresh Data
|
||||
</SharedButton>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* error notice should be a component */}
|
||||
{error && (
|
||||
<SharedBox bg="bg-error-red/10" p={4} rounded="md" border borderColor="border-error-red/20">
|
||||
<SharedStack direction="row" align="center" gap={3}>
|
||||
<SharedIcon icon={ShieldAlert} size={5} color="text-error-red" />
|
||||
<SharedBox>
|
||||
<SharedText weight="bold" color="text-error-red">Operation Failed</SharedText>
|
||||
<SharedText size="sm" color="text-error-red/80">{error}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<ErrorBanner title="Operation Failed" message={error} />
|
||||
)}
|
||||
|
||||
<AdminStatsPanel stats={stats} />
|
||||
@@ -147,9 +145,9 @@ export function AdminUsersTemplate({
|
||||
title="No users found"
|
||||
description="Try adjusting your filters or search query"
|
||||
action={
|
||||
<SharedButton variant="secondary" size="sm" onClick={onClearFilters}>
|
||||
<Button variant="secondary" size="sm" onClick={onClearFilters}>
|
||||
Clear All Filters
|
||||
</SharedButton>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@@ -170,8 +168,8 @@ export function AdminUsersTemplate({
|
||||
actions={bulkActions}
|
||||
onClearSelection={onClearSelection}
|
||||
/>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,14 +67,14 @@ export function DashboardTemplate({
|
||||
<TelemetryPanel title="Active Session">
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" mb={1} block>Next Event</Text>
|
||||
<Text size="xs" variant="low" mb={1} block>Next Event</Text>
|
||||
<Text size="lg" weight="bold" block>{nextRace.track}</Text>
|
||||
<Text size="xs" color="primary-accent" font="mono" block>{nextRace.car}</Text>
|
||||
<Text size="xs" variant="primary" font="mono" block>{nextRace.car}</Text>
|
||||
</Box>
|
||||
<Box textAlign="right">
|
||||
<Text size="xs" color="text-gray-500" mb={1} block>Starts In</Text>
|
||||
<Text size="xl" font="mono" weight="bold" color="warning-amber" block>{nextRace.timeUntil}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
|
||||
<Text size="xs" variant="low" mb={1} block>Starts In</Text>
|
||||
<Text size="xl" font="mono" weight="bold" variant="warning" block>{nextRace.timeUntil}</Text>
|
||||
<Text size="xs" variant="low" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</TelemetryPanel>
|
||||
@@ -85,7 +85,7 @@ export function DashboardTemplate({
|
||||
<RecentActivityTable items={activityItems} />
|
||||
) : (
|
||||
<Box py={8} textAlign="center">
|
||||
<Text italic color="text-gray-500">No recent activity recorded.</Text>
|
||||
<Text italic variant="low">No recent activity recorded.</Text>
|
||||
</Box>
|
||||
)}
|
||||
</TelemetryPanel>
|
||||
@@ -99,18 +99,18 @@ export function DashboardTemplate({
|
||||
{hasLeagueStandings ? (
|
||||
<Stack direction="col" gap={3}>
|
||||
{leagueStandings.map((standing) => (
|
||||
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="rgba(255, 255, 255, 0.1)" pb={2}>
|
||||
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="var(--ui-color-border-muted)" pb={2}>
|
||||
<Box>
|
||||
<Text size="xs" weight="bold" truncate block maxWidth="180px">{standing.leagueName}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
|
||||
<Text size="xs" variant="low" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
|
||||
</Box>
|
||||
<Text size="sm" font="mono" weight="bold" color="primary-accent">{standing.points} PTS</Text>
|
||||
<Text size="sm" font="mono" weight="bold" variant="primary">{standing.points} PTS</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Box py={4} textAlign="center">
|
||||
<Text italic color="text-gray-500">No active championships.</Text>
|
||||
<Text italic variant="low">No active championships.</Text>
|
||||
</Box>
|
||||
)}
|
||||
</TelemetryPanel>
|
||||
@@ -121,11 +121,11 @@ export function DashboardTemplate({
|
||||
<Box key={race.id} cursor="pointer">
|
||||
<Box display="flex" justifyContent="between" alignItems="start" mb={1}>
|
||||
<Text size="xs" weight="bold">{race.track}</Text>
|
||||
<Text size="xs" font="mono" color="text-gray-500">{race.timeUntil}</Text>
|
||||
<Text size="xs" font="mono" variant="low">{race.timeUntil}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-500">{race.car}</Text>
|
||||
<Text size="xs" color="text-gray-500">{race.formattedDate}</Text>
|
||||
<Text size="xs" variant="low">{race.car}</Text>
|
||||
<Text size="xs" variant="low">{race.formattedDate}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -3,35 +3,33 @@
|
||||
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import type { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedLink,
|
||||
SharedText,
|
||||
SharedStack,
|
||||
SharedContainer,
|
||||
SharedIcon
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
|
||||
export function LeagueDetailTemplate({ viewData, children, tabs }: TemplateProps<LeagueDetailViewData> & { children?: React.ReactNode, tabs?: any[] }) {
|
||||
return (
|
||||
<SharedContainer size="lg">
|
||||
<SharedBox paddingY={8}>
|
||||
<SharedStack gap={8}>
|
||||
<SharedBox>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<SharedLink href={routes.public.leagues}>
|
||||
<SharedText size="sm" color="text-gray-400">Leagues</SharedText>
|
||||
</SharedLink>
|
||||
<SharedIcon icon={ChevronRight} size={3} color="text-gray-500" />
|
||||
<SharedText size="sm" color="text-white">{viewData.name}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<Container size="lg">
|
||||
<Box paddingY={8}>
|
||||
<Stack gap={8}>
|
||||
<Box>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Link href={routes.public.leagues}>
|
||||
<Text size="sm" color="text-gray-400">Leagues</Text>
|
||||
</Link>
|
||||
<Icon icon={ChevronRight} size={3} color="text-gray-500" />
|
||||
<Text size="sm" color="text-white">{viewData.name}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
{children}
|
||||
{/* ... rest of the template ... */}
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
|
||||
import { MediaGallery } from '@/components/media/MediaGallery';
|
||||
import { MediaViewData } from '@/lib/view-data/MediaViewData';
|
||||
import { SharedBox, SharedContainer } from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
|
||||
export function MediaTemplate({ viewData }: TemplateProps<MediaViewData>) {
|
||||
const { assets, categories, title, description } = viewData;
|
||||
|
||||
return (
|
||||
<SharedContainer>
|
||||
<SharedBox paddingY={8}>
|
||||
<SharedBox display="flex" flexDirection="col" gap={8}>
|
||||
<Container>
|
||||
<Box paddingY={8}>
|
||||
<Box display="flex" flexDirection="col" gap={8}>
|
||||
<MediaGallery
|
||||
assets={assets}
|
||||
categories={categories}
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
import { UploadDropzone } from '@/components/shared/UploadDropzone';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedContainer,
|
||||
SharedCard
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { MediaMetaPanel, mapMediaMetadata } from '@/ui/MediaMetaPanel';
|
||||
import { MediaPreviewCard } from '@/ui/MediaPreviewCard';
|
||||
@@ -33,18 +31,18 @@ export function ProfileLiveryUploadTemplate({
|
||||
onUpload,
|
||||
}: ProfileLiveryUploadTemplateProps) {
|
||||
return (
|
||||
<SharedContainer size="md">
|
||||
<SharedBox paddingY={8}>
|
||||
<SharedBox mb={6}>
|
||||
<Container size="md">
|
||||
<Box paddingY={8}>
|
||||
<Box mb={6}>
|
||||
<Heading level={1}>Upload livery</Heading>
|
||||
<SharedText color="text-gray-500">
|
||||
<Text color="text-gray-500">
|
||||
Upload your custom car livery. Supported formats: .png, .jpg, .tga
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<SharedBox display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
<SharedBox>
|
||||
<SharedCard>
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
<Box>
|
||||
<Card>
|
||||
<UploadDropzone
|
||||
onFilesSelected={onFilesSelected}
|
||||
accept=".png,.jpg,.jpeg,.tga"
|
||||
@@ -52,25 +50,25 @@ export function ProfileLiveryUploadTemplate({
|
||||
isLoading={isUploading}
|
||||
/>
|
||||
|
||||
<SharedBox mt={6} display="flex" justifyContent="end" gap={3}>
|
||||
<Box mt={6} display="flex" justifyContent="end" gap={3}>
|
||||
<Link href={routes.protected.profileLiveries}>
|
||||
<SharedButton variant="ghost">Cancel</SharedButton>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</Link>
|
||||
<SharedButton
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!selectedFile || isUploading}
|
||||
onClick={onUpload}
|
||||
isLoading={isUploading}
|
||||
>
|
||||
Upload Livery
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</SharedBox>
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<SharedBox>
|
||||
<Box>
|
||||
{previewUrl ? (
|
||||
<SharedBox display="flex" flexDirection="col" gap={6}>
|
||||
<Box display="flex" flexDirection="col" gap={6}>
|
||||
<MediaPreviewCard
|
||||
type="image"
|
||||
src={previewUrl}
|
||||
@@ -88,17 +86,17 @@ export function ProfileLiveryUploadTemplate({
|
||||
createdAt: new Date(),
|
||||
})}
|
||||
/>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
) : (
|
||||
<SharedCard center p={12}>
|
||||
<SharedText color="text-gray-500" align="center">
|
||||
<Card center p={12}>
|
||||
<Text color="text-gray-500" align="center">
|
||||
Select a file to see preview and details
|
||||
</SharedText>
|
||||
</SharedCard>
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ export function ProfileTemplate({
|
||||
return (
|
||||
<Stack align="center" gap={4} mb={8}>
|
||||
<Surface variant="muted" rounded="xl" border padding={4}>
|
||||
<Icon icon={User} size={8} color="var(--color-primary)" />
|
||||
<Icon icon={User} size={8} color="var(--ui-color-intent-primary)" />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Heading level={1}>Create Your Driver Profile</Heading>
|
||||
<Text color="text-gray-400">Join the GridPilot community and start your racing journey</Text>
|
||||
<Text variant="low">Join the GridPilot community and start your racing journey</Text>
|
||||
</Box>
|
||||
|
||||
<Box maxWidth="42rem" mx="auto" width="100%">
|
||||
@@ -51,7 +51,7 @@ export function ProfileTemplate({
|
||||
<Stack gap={6}>
|
||||
<Box>
|
||||
<Heading level={2}>Get Started</Heading>
|
||||
<Text size="sm" color="text-gray-400">
|
||||
<Text size="sm" variant="low">
|
||||
Create your driver profile to join leagues, compete in races, and connect with other drivers.
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -110,7 +110,7 @@ export function ProfileTemplate({
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Heading level={3} id="achievements-heading">Achievements</Heading>
|
||||
<Text size="sm" color="text-gray-500">{viewData.extendedProfile.achievements.length} earned</Text>
|
||||
<Text size="sm" variant="low">{viewData.extendedProfile.achievements.length} earned</Text>
|
||||
</Stack>
|
||||
<AchievementGrid
|
||||
achievements={viewData.extendedProfile.achievements.map(a => ({
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedIcon,
|
||||
SharedCard,
|
||||
SharedContainer
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Link as UILink } from '@/ui/Link';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
@@ -99,114 +97,114 @@ export function ProtestDetailTemplate({
|
||||
const daysSinceFiled = Math.floor((Date.now() - new Date(submittedAt).getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
return (
|
||||
<SharedBox minHeight="100vh">
|
||||
<SharedContainer size="lg">
|
||||
<SharedBox paddingY={8}>
|
||||
<Box minHeight="100vh">
|
||||
<Container size="lg">
|
||||
<Box paddingY={8}>
|
||||
{/* Compact Header */}
|
||||
<SharedBox mb={6}>
|
||||
<SharedStack direction="row" align="center" gap={3} mb={4}>
|
||||
<Box mb={6}>
|
||||
<Stack direction="row" align="center" gap={3} mb={4}>
|
||||
<UILink href={routes.league.stewarding(leagueId)}>
|
||||
<SharedIcon icon={ArrowLeft} size={5} color="text-gray-400" />
|
||||
<Icon icon={ArrowLeft} size={5} color="text-gray-400" />
|
||||
</UILink>
|
||||
<SharedStack direction="row" align="center" gap={3} flexGrow={1}>
|
||||
<Stack direction="row" align="center" gap={3} flexGrow={1}>
|
||||
<Heading level={1}>Protest Review</Heading>
|
||||
<SharedBox display="flex" alignItems="center" gap={1.5} px={2.5} py={1} rounded="full" fontSize="0.75rem" weight="medium" border bg={statusConfig.bg} color={statusConfig.color} borderColor={statusConfig.borderColor}>
|
||||
<SharedIcon icon={StatusIcon} size={3} />
|
||||
<SharedText>{statusConfig.label}</SharedText>
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="center" gap={1.5} px={2.5} py={1} rounded="full" fontSize="0.75rem" weight="medium" border bg={statusConfig.bg} color={statusConfig.color} borderColor={statusConfig.borderColor}>
|
||||
<Icon icon={StatusIcon} size={3} />
|
||||
<Text>{statusConfig.label}</Text>
|
||||
</Box>
|
||||
{daysSinceFiled > 2 && isPending && (
|
||||
<SharedBox display="flex" alignItems="center" gap={1} px={2} py={0.5} fontSize="0.75rem" weight="medium" bg="bg-red-500/20" color="text-red-400" rounded="full">
|
||||
<SharedIcon icon={AlertTriangle} size={3} />
|
||||
<SharedText>{daysSinceFiled}d old</SharedText>
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="center" gap={1} px={2} py={0.5} fontSize="0.75rem" weight="medium" bg="bg-red-500/20" color="text-red-400" rounded="full">
|
||||
<Icon icon={AlertTriangle} size={3} />
|
||||
<Text>{daysSinceFiled}d old</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SharedStack>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Main Layout: Feed + Sidebar */}
|
||||
<Grid cols={12} gap={6}>
|
||||
{/* Left Sidebar - Incident Info */}
|
||||
<GridItem colSpan={{ base: 12, lg: 3 }}>
|
||||
<SharedStack gap={4}>
|
||||
<Stack gap={4}>
|
||||
{/* Drivers Involved */}
|
||||
<SharedCard>
|
||||
<SharedBox p={4}>
|
||||
<Card>
|
||||
<Box p={4}>
|
||||
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Parties Involved</Heading>
|
||||
|
||||
<SharedStack gap={3}>
|
||||
<Stack gap={3}>
|
||||
{/* Protesting Driver */}
|
||||
<UILink href={routes.driver.detail(protestingDriver?.id || '')} block>
|
||||
<SharedBox display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-blue-500/50" hoverBg="bg-blue-500/5" transition cursor="pointer">
|
||||
<SharedBox w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<SharedIcon icon={User} size={5} color="text-blue-400" />
|
||||
</SharedBox>
|
||||
<SharedBox flexGrow={1} minWidth="0">
|
||||
<SharedText size="xs" color="text-blue-400" weight="medium" block>Protesting</SharedText>
|
||||
<SharedText size="sm" weight="semibold" color="text-white" truncate block>{protestingDriver?.name || 'Unknown'}</SharedText>
|
||||
</SharedBox>
|
||||
<SharedIcon icon={ExternalLink} size={3} color="text-gray-500" />
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-blue-500/50" hoverBg="bg-blue-500/5" transition cursor="pointer">
|
||||
<Box w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<Icon icon={User} size={5} color="text-blue-400" />
|
||||
</Box>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Text size="xs" color="text-blue-400" weight="medium" block>Protesting</Text>
|
||||
<Text size="sm" weight="semibold" color="text-white" truncate block>{protestingDriver?.name || 'Unknown'}</Text>
|
||||
</Box>
|
||||
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
|
||||
</Box>
|
||||
</UILink>
|
||||
|
||||
{/* Accused Driver */}
|
||||
<UILink href={routes.driver.detail(accusedDriver?.id || '')} block>
|
||||
<SharedBox display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-orange-500/50" hoverBg="bg-orange-500/5" transition cursor="pointer">
|
||||
<SharedBox w={10} h={10} rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<SharedIcon icon={User} size={5} color="text-orange-400" />
|
||||
</SharedBox>
|
||||
<SharedBox flexGrow={1} minWidth="0">
|
||||
<SharedText size="xs" color="text-orange-400" weight="medium" block>Accused</SharedText>
|
||||
<SharedText size="sm" weight="semibold" color="text-white" truncate block>{accusedDriver?.name || 'Unknown'}</SharedText>
|
||||
</SharedBox>
|
||||
<SharedIcon icon={ExternalLink} size={3} color="text-gray-500" />
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-orange-500/50" hoverBg="bg-orange-500/5" transition cursor="pointer">
|
||||
<Box w={10} h={10} rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<Icon icon={User} size={5} color="text-orange-400" />
|
||||
</Box>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Text size="xs" color="text-orange-400" weight="medium" block>Accused</Text>
|
||||
<Text size="sm" weight="semibold" color="text-white" truncate block>{accusedDriver?.name || 'Unknown'}</Text>
|
||||
</Box>
|
||||
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
|
||||
</Box>
|
||||
</UILink>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Race Info */}
|
||||
<SharedCard>
|
||||
<SharedBox p={4}>
|
||||
<Card>
|
||||
<Box p={4}>
|
||||
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Race Details</Heading>
|
||||
|
||||
<SharedBox marginBottom={3}>
|
||||
<Box marginBottom={3}>
|
||||
<UILink
|
||||
href={routes.race.detail(race?.id || '')}
|
||||
block
|
||||
>
|
||||
<SharedBox p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-primary-blue/50" hoverBg="bg-primary-blue/5" transition>
|
||||
<SharedBox display="flex" alignItems="center" justifyContent="between">
|
||||
<SharedText size="sm" weight="medium" color="text-white">{race?.name || 'Unknown Race'}</SharedText>
|
||||
<SharedIcon icon={ExternalLink} size={3} color="text-gray-500" />
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
<Box p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" hoverBorderColor="border-primary-blue/50" hoverBg="bg-primary-blue/5" transition>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="sm" weight="medium" color="text-white">{race?.name || 'Unknown Race'}</Text>
|
||||
<Icon icon={ExternalLink} size={3} color="text-gray-500" />
|
||||
</Box>
|
||||
</Box>
|
||||
</UILink>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
|
||||
<SharedStack gap={2}>
|
||||
<SharedBox display="flex" alignItems="center" gap={2}>
|
||||
<SharedIcon icon={MapPin} size={4} color="text-gray-500" />
|
||||
<SharedText size="sm" color="text-gray-300">{race?.name || 'Unknown Track'}</SharedText>
|
||||
</SharedBox>
|
||||
<SharedBox display="flex" alignItems="center" gap={2}>
|
||||
<SharedIcon icon={Calendar} size={4} color="text-gray-500" />
|
||||
<SharedText size="sm" color="text-gray-300">{race?.formattedDate || (race?.scheduledAt ? new Date(race.scheduledAt).toLocaleDateString() : 'Unknown Date')}</SharedText>
|
||||
</SharedBox>
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={MapPin} size={4} color="text-gray-500" />
|
||||
<Text size="sm" color="text-gray-300">{race?.name || 'Unknown Track'}</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Calendar} size={4} color="text-gray-500" />
|
||||
<Text size="sm" color="text-gray-300">{race?.formattedDate || (race?.scheduledAt ? new Date(race.scheduledAt).toLocaleDateString() : 'Unknown Date')}</Text>
|
||||
</Box>
|
||||
{protest.incident?.lap && (
|
||||
<SharedBox display="flex" alignItems="center" gap={2}>
|
||||
<SharedIcon icon={Flag} size={4} color="text-gray-500" />
|
||||
<SharedText size="sm" color="text-gray-300">Lap {protest.incident.lap}</SharedText>
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Flag} size={4} color="text-gray-500" />
|
||||
<Text size="sm" color="text-gray-300">Lap {protest.incident.lap}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{protest.proofVideoUrl && (
|
||||
<SharedCard>
|
||||
<SharedBox p={4}>
|
||||
<Card>
|
||||
<Box p={4}>
|
||||
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Evidence</Heading>
|
||||
<UILink
|
||||
href={protest.proofVideoUrl}
|
||||
@@ -214,133 +212,133 @@ export function ProtestDetailTemplate({
|
||||
rel="noopener noreferrer"
|
||||
block
|
||||
>
|
||||
<SharedBox display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" color="text-primary-blue" hoverBg="bg-primary-blue/20" transition>
|
||||
<SharedIcon icon={Video} size={4} />
|
||||
<SharedText size="sm" weight="medium" flexGrow={1}>Watch Video</SharedText>
|
||||
<SharedIcon icon={ExternalLink} size={3} />
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" color="text-primary-blue" hoverBg="bg-primary-blue/20" transition>
|
||||
<Icon icon={Video} size={4} />
|
||||
<Text size="sm" weight="medium" flexGrow={1}>Watch Video</Text>
|
||||
<Icon icon={ExternalLink} size={3} />
|
||||
</Box>
|
||||
</UILink>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Quick Stats */}
|
||||
<SharedCard>
|
||||
<SharedBox p={4}>
|
||||
<Card>
|
||||
<Box p={4}>
|
||||
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Timeline</Heading>
|
||||
<SharedStack gap={2}>
|
||||
<SharedBox display="flex" justifyContent="between">
|
||||
<SharedText size="sm" color="text-gray-500">Filed</SharedText>
|
||||
<SharedText size="sm" color="text-gray-300">{new Date(submittedAt).toLocaleDateString()}</SharedText>
|
||||
</SharedBox>
|
||||
<SharedBox display="flex" justifyContent="between">
|
||||
<SharedText size="sm" color="text-gray-500">Age</SharedText>
|
||||
<SharedText size="sm" color={daysSinceFiled > 2 ? 'text-red-400' : 'text-gray-300'}>{daysSinceFiled} days</SharedText>
|
||||
</SharedBox>
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="sm" color="text-gray-500">Filed</Text>
|
||||
<Text size="sm" color="text-gray-300">{new Date(submittedAt).toLocaleDateString()}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="sm" color="text-gray-500">Age</Text>
|
||||
<Text size="sm" color={daysSinceFiled > 2 ? 'text-red-400' : 'text-gray-300'}>{daysSinceFiled} days</Text>
|
||||
</Box>
|
||||
{protest.reviewedAt && (
|
||||
<SharedBox display="flex" justifyContent="between">
|
||||
<SharedText size="sm" color="text-gray-500">Resolved</SharedText>
|
||||
<SharedText size="sm" color="text-gray-300">{new Date(protest.reviewedAt).toLocaleDateString()}</SharedText>
|
||||
</SharedBox>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="sm" color="text-gray-500">Resolved</Text>
|
||||
<Text size="sm" color="text-gray-300">{new Date(protest.reviewedAt).toLocaleDateString()}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</SharedStack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Card>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
|
||||
{/* Center - Discussion Feed */}
|
||||
<GridItem colSpan={12} lgSpan={6}>
|
||||
<SharedStack gap={4}>
|
||||
<Stack gap={4}>
|
||||
{/* Timeline / Feed */}
|
||||
<SharedCard>
|
||||
<SharedBox borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={4}>
|
||||
<Card>
|
||||
<Box borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={4}>
|
||||
<Heading level={2}>Discussion</Heading>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
|
||||
<SharedStack gap={0}>
|
||||
<Stack gap={0}>
|
||||
{/* Initial Protest Filing */}
|
||||
<SharedBox p={4}>
|
||||
<SharedBox display="flex" gap={3}>
|
||||
<SharedBox w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<SharedIcon icon={AlertCircle} size={5} color="text-blue-400" />
|
||||
</SharedBox>
|
||||
<SharedBox flexGrow={1} minWidth="0">
|
||||
<SharedBox display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<SharedText weight="semibold" color="text-white" size="sm">{protestingDriver?.name || 'Unknown'}</SharedText>
|
||||
<SharedText size="xs" color="text-blue-400" weight="medium">filed protest</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500">•</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500">{new Date(submittedAt).toLocaleString()}</SharedText>
|
||||
</SharedBox>
|
||||
<Box p={4}>
|
||||
<Box display="flex" gap={3}>
|
||||
<Box w={10} h={10} rounded="full" bg="bg-blue-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<Icon icon={AlertCircle} size={5} color="text-blue-400" />
|
||||
</Box>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<Text weight="semibold" color="text-white" size="sm">{protestingDriver?.name || 'Unknown'}</Text>
|
||||
<Text size="xs" color="text-blue-400" weight="medium">filed protest</Text>
|
||||
<Text size="xs" color="text-gray-500">•</Text>
|
||||
<Text size="xs" color="text-gray-500">{new Date(submittedAt).toLocaleString()}</Text>
|
||||
</Box>
|
||||
|
||||
<SharedBox bg="bg-deep-graphite" rounded="lg" p={4} border borderColor="border-charcoal-outline">
|
||||
<SharedText size="sm" color="text-gray-300" block mb={3}>{protest.description || protestDetail.incident?.description}</SharedText>
|
||||
<Box bg="bg-deep-graphite" rounded="lg" p={4} border borderColor="border-charcoal-outline">
|
||||
<Text size="sm" color="text-gray-300" block mb={3}>{protest.description || protestDetail.incident?.description}</Text>
|
||||
|
||||
{(protest.comment || protestDetail.comment) && (
|
||||
<SharedBox mt={3} pt={3} borderTop borderColor="border-charcoal-outline/50">
|
||||
<SharedText size="xs" color="text-gray-500" block mb={1}>Additional details:</SharedText>
|
||||
<SharedText size="sm" color="text-gray-400">{protest.comment || protestDetail.comment}</SharedText>
|
||||
</SharedBox>
|
||||
<Box mt={3} pt={3} borderTop borderColor="border-charcoal-outline/50">
|
||||
<Text size="xs" color="text-gray-500" block mb={1}>Additional details:</Text>
|
||||
<Text size="sm" color="text-gray-400">{protest.comment || protestDetail.comment}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Defense placeholder */}
|
||||
{protest.status === 'awaiting_defense' && (
|
||||
<SharedBox p={4} bg="bg-purple-500/5">
|
||||
<SharedBox display="flex" gap={3}>
|
||||
<SharedBox w={10} h={10} rounded="full" bg="bg-purple-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<SharedIcon icon={MessageCircle} size={5} color="text-purple-400" />
|
||||
</SharedBox>
|
||||
<SharedBox flexGrow={1}>
|
||||
<SharedText size="sm" color="text-purple-400" weight="medium" block mb={1}>Defense Requested</SharedText>
|
||||
<SharedText size="sm" color="text-gray-400">Waiting for {accusedDriver?.name || 'the accused driver'} to submit their defense...</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
<Box p={4} bg="bg-purple-500/5">
|
||||
<Box display="flex" gap={3}>
|
||||
<Box w={10} h={10} rounded="full" bg="bg-purple-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<Icon icon={MessageCircle} size={5} color="text-purple-400" />
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text size="sm" color="text-purple-400" weight="medium" block mb={1}>Defense Requested</Text>
|
||||
<Text size="sm" color="text-gray-400">Waiting for {accusedDriver?.name || 'the accused driver'} to submit their defense...</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Decision (if resolved) */}
|
||||
{(protest.status === 'upheld' || protest.status === 'dismissed') && protest.decisionNotes && (
|
||||
<SharedBox p={4} bg={protest.status === 'upheld' ? 'bg-red-500/5' : 'bg-gray-500/5'}>
|
||||
<SharedBox display="flex" gap={3}>
|
||||
<SharedBox w={10} h={10} rounded="full" display="flex" alignItems="center" justifyContent="center" flexShrink={0} bg={protest.status === 'upheld' ? 'bg-red-500/20' : 'bg-gray-500/20'}>
|
||||
<SharedIcon icon={Gavel} size={5} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'} />
|
||||
</SharedBox>
|
||||
<SharedBox flexGrow={1} minWidth="0">
|
||||
<SharedBox display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<SharedText weight="semibold" color="text-white" size="sm">Steward Decision</SharedText>
|
||||
<SharedText size="xs" weight="medium" color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
|
||||
<Box p={4} bg={protest.status === 'upheld' ? 'bg-red-500/5' : 'bg-gray-500/5'}>
|
||||
<Box display="flex" gap={3}>
|
||||
<Box w={10} h={10} rounded="full" display="flex" alignItems="center" justifyContent="center" flexShrink={0} bg={protest.status === 'upheld' ? 'bg-red-500/20' : 'bg-gray-500/20'}>
|
||||
<Icon icon={Gavel} size={5} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'} />
|
||||
</Box>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<Text weight="semibold" color="text-white" size="sm">Steward Decision</Text>
|
||||
<Text size="xs" weight="medium" color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
|
||||
{protest.status === 'upheld' ? 'Protest Upheld' : 'Protest Dismissed'}
|
||||
</SharedText>
|
||||
</Text>
|
||||
{protest.reviewedAt && (
|
||||
<>
|
||||
<SharedText size="xs" color="text-gray-500">•</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500">{new Date(protest.reviewedAt).toLocaleString()}</SharedText>
|
||||
<Text size="xs" color="text-gray-500">•</Text>
|
||||
<Text size="xs" color="text-gray-500">{new Date(protest.reviewedAt).toLocaleString()}</Text>
|
||||
</>
|
||||
)}
|
||||
</SharedBox>
|
||||
</Box>
|
||||
|
||||
<SharedBox rounded="lg" p={4} border bg={protest.status === 'upheld' ? 'bg-red-500/10' : 'bg-gray-500/10'} borderColor={protest.status === 'upheld' ? 'border-red-500/20' : 'border-gray-500/20'}>
|
||||
<SharedText size="sm" color="text-gray-300">{protest.decisionNotes}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
<Box rounded="lg" p={4} border bg={protest.status === 'upheld' ? 'bg-red-500/10' : 'bg-gray-500/10'} borderColor={protest.status === 'upheld' ? 'border-red-500/20' : 'border-gray-500/20'}>
|
||||
<Text size="sm" color="text-gray-300">{protest.decisionNotes}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</SharedStack>
|
||||
</Stack>
|
||||
|
||||
{/* Add Comment */}
|
||||
{isPending && (
|
||||
<SharedBox p={4} borderTop borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<SharedBox display="flex" gap={3}>
|
||||
<SharedBox w={10} h={10} rounded="full" bg="bg-iron-gray" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<SharedIcon icon={User} size={5} color="text-gray-500" />
|
||||
</SharedBox>
|
||||
<SharedBox flexGrow={1}>
|
||||
<SharedBox as="textarea"
|
||||
<Box p={4} borderTop borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
|
||||
<Box display="flex" gap={3}>
|
||||
<Box w={10} h={10} rounded="full" bg="bg-iron-gray" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
|
||||
<Icon icon={User} size={5} color="text-gray-500" />
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Box as="textarea"
|
||||
value={newComment}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewComment(e.target.value)}
|
||||
placeholder="Add a comment or request more information..."
|
||||
@@ -355,102 +353,102 @@ export function ProtestDetailTemplate({
|
||||
color="text-white"
|
||||
fontSize="sm"
|
||||
/>
|
||||
<SharedBox display="flex" justifyContent="end" mt={2}>
|
||||
<SharedButton variant="secondary" disabled={!newComment.trim()}>
|
||||
<SharedIcon icon={Send} size={3} style={{ marginRight: '0.25rem' }} />
|
||||
<Box display="flex" justifyContent="end" mt={2}>
|
||||
<Button variant="secondary" disabled={!newComment.trim()}>
|
||||
<Icon icon={Send} size={3} style={{ marginRight: '0.25rem' }} />
|
||||
Comment
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</SharedCard>
|
||||
</SharedStack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
|
||||
{/* Right Sidebar - Actions */}
|
||||
<GridItem colSpan={12} lgSpan={3}>
|
||||
<SharedStack gap={4}>
|
||||
<Stack gap={4}>
|
||||
{isPending && (
|
||||
<>
|
||||
{/* Quick Actions */}
|
||||
<SharedCard>
|
||||
<SharedBox p={4}>
|
||||
<Card>
|
||||
<Box p={4}>
|
||||
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Actions</Heading>
|
||||
|
||||
<SharedStack gap={2}>
|
||||
<SharedButton
|
||||
<Stack gap={2}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={onRequestDefense}
|
||||
>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<SharedIcon icon={MessageCircle} size={4} />
|
||||
<SharedText>Request Defense</SharedText>
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={MessageCircle} size={4} />
|
||||
<Text>Request Defense</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
|
||||
<SharedButton
|
||||
<Button
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={() => setShowDecisionPanel(!showDecisionPanel)}
|
||||
>
|
||||
<SharedStack direction="row" align="center" gap={2} fullWidth>
|
||||
<SharedIcon icon={Gavel} size={4} />
|
||||
<SharedText>Make Decision</SharedText>
|
||||
<SharedBox ml="auto" transition style={{ transform: showDecisionPanel ? 'rotate(180deg)' : 'none' }}>
|
||||
<SharedIcon icon={ChevronDown} size={4} />
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
<Stack direction="row" align="center" gap={2} fullWidth>
|
||||
<Icon icon={Gavel} size={4} />
|
||||
<Text>Make Decision</Text>
|
||||
<Box ml="auto" transition style={{ transform: showDecisionPanel ? 'rotate(180deg)' : 'none' }}>
|
||||
<Icon icon={ChevronDown} size={4} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Decision Panel */}
|
||||
{showDecisionPanel && (
|
||||
<SharedCard>
|
||||
<SharedBox p={4}>
|
||||
<Card>
|
||||
<Box p={4}>
|
||||
<Heading level={3} fontSize="xs" weight="semibold" color="text-gray-500" mb={3}>Stewarding Decision</Heading>
|
||||
|
||||
{/* Decision Selection */}
|
||||
<Grid cols={2} gap={2} mb={4}>
|
||||
<SharedBox padding={3} border borderColor={decision === 'uphold' ? 'border-racing-red' : 'border-charcoal-outline'} bg={decision === 'uphold' ? 'bg-racing-red/10' : 'transparent'} rounded="lg">
|
||||
<SharedButton
|
||||
<Box padding={3} border borderColor={decision === 'uphold' ? 'border-racing-red' : 'border-charcoal-outline'} bg={decision === 'uphold' ? 'bg-racing-red/10' : 'transparent'} rounded="lg">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setDecision('uphold')}
|
||||
fullWidth
|
||||
>
|
||||
<SharedStack align="center" gap={1}>
|
||||
<SharedIcon icon={CheckCircle} size={5} color={decision === 'uphold' ? 'text-red-400' : 'text-gray-500'} />
|
||||
<SharedText size="xs" weight="medium" color={decision === 'uphold' ? 'text-red-400' : 'text-gray-400'}>Uphold</SharedText>
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
<SharedBox padding={3} border borderColor={decision === 'dismiss' ? 'border-gray-500' : 'border-charcoal-outline'} bg={decision === 'dismiss' ? 'bg-gray-500/10' : 'transparent'} rounded="lg">
|
||||
<SharedButton
|
||||
<Stack align="center" gap={1}>
|
||||
<Icon icon={CheckCircle} size={5} color={decision === 'uphold' ? 'text-red-400' : 'text-gray-500'} />
|
||||
<Text size="xs" weight="medium" color={decision === 'uphold' ? 'text-red-400' : 'text-gray-400'}>Uphold</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
<Box padding={3} border borderColor={decision === 'dismiss' ? 'border-gray-500' : 'border-charcoal-outline'} bg={decision === 'dismiss' ? 'bg-gray-500/10' : 'transparent'} rounded="lg">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setDecision('dismiss')}
|
||||
fullWidth
|
||||
>
|
||||
<SharedStack align="center" gap={1}>
|
||||
<SharedIcon icon={XCircle} size={5} color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-500'} />
|
||||
<SharedText size="xs" weight="medium" color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-400'}>Dismiss</SharedText>
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
<Stack align="center" gap={1}>
|
||||
<Icon icon={XCircle} size={5} color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-500'} />
|
||||
<Text size="xs" weight="medium" color={decision === 'dismiss' ? 'text-gray-300' : 'text-gray-400'}>Dismiss</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Penalty Selection (if upholding) */}
|
||||
{decision === 'uphold' && (
|
||||
<SharedBox mb={4}>
|
||||
<SharedText as="label" size="xs" weight="medium" color="text-gray-400" block mb={2}>Penalty Type</SharedText>
|
||||
<Box mb={4}>
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={2}>Penalty Type</Text>
|
||||
|
||||
{penaltyTypes.length === 0 ? (
|
||||
<SharedText size="xs" color="text-gray-500">
|
||||
<Text size="xs" color="text-gray-500">
|
||||
Loading penalty types...
|
||||
</SharedText>
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<Grid cols={2} gap={2}>
|
||||
@@ -458,8 +456,8 @@ export function ProtestDetailTemplate({
|
||||
const Icon = penalty.icon;
|
||||
const isSelected = penaltyType === penalty.type;
|
||||
return (
|
||||
<SharedBox key={penalty.type} padding={2} border borderColor={isSelected ? undefined : 'border-charcoal-outline'} bg={isSelected ? undefined : 'bg-iron-gray/30'} rounded="lg">
|
||||
<SharedButton
|
||||
<Box key={penalty.type} padding={2} border borderColor={isSelected ? undefined : 'border-charcoal-outline'} bg={isSelected ? undefined : 'bg-iron-gray/30'} rounded="lg">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setPenaltyType(penalty.type);
|
||||
@@ -468,24 +466,24 @@ export function ProtestDetailTemplate({
|
||||
fullWidth
|
||||
title={penalty.description}
|
||||
>
|
||||
<SharedStack align="start" gap={0.5}>
|
||||
<SharedIcon icon={Icon} size={3.5} color={isSelected ? penalty.color : 'text-gray-500'} />
|
||||
<SharedText size="xs" weight="medium" fontSize="10px" color={isSelected ? penalty.color : 'text-gray-500'}>
|
||||
<Stack align="start" gap={0.5}>
|
||||
<Icon icon={Icon} size={3.5} color={isSelected ? penalty.color : 'text-gray-500'} />
|
||||
<Text size="xs" weight="medium" fontSize="10px" color={isSelected ? penalty.color : 'text-gray-500'}>
|
||||
{penalty.label}
|
||||
</SharedText>
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{selectedPenalty?.requiresValue && (
|
||||
<SharedBox mt={3}>
|
||||
<SharedText as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>
|
||||
<Box mt={3}>
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>
|
||||
Value ({selectedPenalty.valueLabel})
|
||||
</SharedText>
|
||||
<SharedBox as="input"
|
||||
</Text>
|
||||
<Box as="input"
|
||||
type="number"
|
||||
value={penaltyValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPenaltyValue(Number(e.target.value))}
|
||||
@@ -500,17 +498,17 @@ export function ProtestDetailTemplate({
|
||||
color="text-white"
|
||||
fontSize="sm"
|
||||
/>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SharedBox>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Steward Notes */}
|
||||
<SharedBox mb={4}>
|
||||
<SharedText as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>Decision Reasoning *</SharedText>
|
||||
<SharedBox as="textarea"
|
||||
<Box mb={4}>
|
||||
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1}>Decision Reasoning *</Text>
|
||||
<Box as="textarea"
|
||||
value={stewardNotes}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setStewardNotes(e.target.value)}
|
||||
placeholder="Explain your decision..."
|
||||
@@ -525,42 +523,42 @@ export function ProtestDetailTemplate({
|
||||
color="text-white"
|
||||
fontSize="sm"
|
||||
/>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
|
||||
{/* Submit */}
|
||||
<SharedButton
|
||||
<Button
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={onSubmitDecision}
|
||||
disabled={!decision || !stewardNotes.trim() || submitting}
|
||||
>
|
||||
{submitting ? 'Submitting...' : 'Submit Decision'}
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Already Resolved Info */}
|
||||
{!isPending && (
|
||||
<SharedCard>
|
||||
<SharedBox p={4} textAlign="center">
|
||||
<SharedBox py={4} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
|
||||
<SharedIcon icon={Gavel} size={8} style={{ margin: '0 auto 0.5rem auto' }} />
|
||||
<SharedText weight="semibold" block>Case Closed</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500" block mt={1}>
|
||||
<Card>
|
||||
<Box p={4} textAlign="center">
|
||||
<Box py={4} color={protest.status === 'upheld' ? 'text-red-400' : 'text-gray-400'}>
|
||||
<Icon icon={Gavel} size={8} style={{ margin: '0 auto 0.5rem auto' }} />
|
||||
<Text weight="semibold" block>Case Closed</Text>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
{protest.status === 'upheld' ? 'Protest was upheld' : 'Protest was dismissed'}
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
</SharedStack>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { RacesAllLayout, RacesAllStats } from '@/components/races/RacesAllLayout
|
||||
import { RaceScheduleSection } from '@/components/races/RacesLayout';
|
||||
import type { SessionStatus } from '@/components/races/SessionStatusBadge';
|
||||
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
|
||||
import { SharedPagination, SharedText, SharedBox } from '@/components/shared/UIComponents';
|
||||
import { Pagination } from '@/ui/Pagination';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
|
||||
export type StatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
|
||||
@@ -76,7 +78,7 @@ export function RacesAllTemplate({
|
||||
/>
|
||||
}
|
||||
pagination={
|
||||
<SharedPagination
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
@@ -85,9 +87,9 @@ export function RacesAllTemplate({
|
||||
>
|
||||
<RaceScheduleSection title="Race Schedule">
|
||||
{races.length === 0 ? (
|
||||
<SharedBox p={12} textAlign="center">
|
||||
<SharedText color="text-gray-500">No races found matching your criteria.</SharedText>
|
||||
</SharedBox>
|
||||
<Box p={12} textAlign="center">
|
||||
<Text color="text-gray-500">No races found matching your criteria.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<RaceScheduleTable
|
||||
races={races.map(race => ({
|
||||
|
||||
@@ -7,15 +7,13 @@ import { SponsorDashboardHeader } from '@/components/sponsors/SponsorDashboardHe
|
||||
import { SponsorStatusChip } from '@/components/sponsors/SponsorStatusChip';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { siteConfig } from '@/lib/siteConfig';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedIcon,
|
||||
SharedCard,
|
||||
SharedContainer
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
@@ -187,23 +185,23 @@ export function SponsorLeagueDetailTemplate({
|
||||
];
|
||||
|
||||
return (
|
||||
<SharedContainer size="lg">
|
||||
<SharedBox paddingY={8}>
|
||||
<SharedStack gap={8}>
|
||||
<Container size="lg">
|
||||
<Box paddingY={8}>
|
||||
<Stack gap={8}>
|
||||
{/* Breadcrumb */}
|
||||
<SharedBox>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<Box>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Link href={routes.sponsor.dashboard}>
|
||||
<SharedText size="sm" color="text-gray-400">Dashboard</SharedText>
|
||||
<Text size="sm" color="text-gray-400">Dashboard</Text>
|
||||
</Link>
|
||||
<SharedText size="sm" color="text-gray-500">/</SharedText>
|
||||
<Text size="sm" color="text-gray-500">/</Text>
|
||||
<Link href={routes.sponsor.leagues}>
|
||||
<SharedText size="sm" color="text-gray-400">Leagues</SharedText>
|
||||
<Text size="sm" color="text-gray-400">Leagues</Text>
|
||||
</Link>
|
||||
<SharedText size="sm" color="text-gray-500">/</SharedText>
|
||||
<SharedText size="sm" color="text-white">{league.name}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<Text size="sm" color="text-gray-500">/</Text>
|
||||
<Text size="sm" color="text-white">{league.name}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Header */}
|
||||
<SponsorDashboardHeader
|
||||
@@ -215,10 +213,10 @@ export function SponsorLeagueDetailTemplate({
|
||||
<BillingSummaryPanel stats={billingStats} />
|
||||
|
||||
{/* Tabs */}
|
||||
<SharedBox borderBottom borderColor="border-neutral-800">
|
||||
<SharedStack direction="row" gap={6}>
|
||||
<Box borderBottom borderColor="border-neutral-800">
|
||||
<Stack direction="row" gap={6}>
|
||||
{(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => (
|
||||
<SharedBox
|
||||
<Box
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
style={{ paddingBottom: '0.75rem' }}
|
||||
@@ -227,148 +225,148 @@ export function SponsorLeagueDetailTemplate({
|
||||
borderColor={activeTab === tab ? 'border-primary-blue' : 'border-transparent'}
|
||||
color={activeTab === tab ? 'text-primary-blue' : 'text-gray-400'}
|
||||
>
|
||||
<SharedText size="sm" weight="medium" className="uppercase">
|
||||
<Text size="sm" weight="medium" className="uppercase">
|
||||
{tab === 'sponsor' ? '🎯 Become a Sponsor' : tab}
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'overview' && (
|
||||
<Grid cols={2} gap={6}>
|
||||
<SharedCard>
|
||||
<SharedBox mb={4}>
|
||||
<SharedStack direction="row" align="center" gap={3}>
|
||||
<SharedIcon icon={Trophy} size={5} color="#3b82f6" />
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Icon icon={Trophy} size={5} color="#3b82f6" />
|
||||
<Heading level={2}>
|
||||
League Information
|
||||
</Heading>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<SharedStack gap={3}>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack gap={3}>
|
||||
<InfoRow label="Platform" value={league.game} />
|
||||
<InfoRow label="Season" value={league.season} />
|
||||
<InfoRow label="Duration" value="Oct 2025 - Feb 2026" />
|
||||
<InfoRow label="Drivers" value={league.formattedDrivers} />
|
||||
<InfoRow label="Races" value={league.formattedRaces} last />
|
||||
</SharedStack>
|
||||
</SharedCard>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<SharedCard>
|
||||
<SharedBox mb={4}>
|
||||
<Heading level={2} icon={<SharedIcon icon={TrendingUp} size={5} color="#10b981" />}>
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Heading level={2} icon={<Icon icon={TrendingUp} size={5} color="#10b981" />}>
|
||||
Sponsorship Value
|
||||
</Heading>
|
||||
</SharedBox>
|
||||
<SharedStack gap={3}>
|
||||
</Box>
|
||||
<Stack gap={3}>
|
||||
<InfoRow label="Total Season Views" value={league.formattedTotalImpressions} />
|
||||
<InfoRow label="Projected Total" value={league.formattedProjectedTotal} />
|
||||
<InfoRow label="Main Sponsor CPM" value={league.formattedMainSponsorCpm} color="text-performance-green" />
|
||||
<InfoRow label="Engagement Rate" value={league.formattedEngagement} />
|
||||
<InfoRow label="League Rating" value={league.formattedRating} last />
|
||||
</SharedStack>
|
||||
</SharedCard>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{league.nextRace && (
|
||||
<GridItem colSpan={2}>
|
||||
<SharedCard>
|
||||
<SharedBox mb={4}>
|
||||
<Heading level={2} icon={<SharedIcon icon={Flag} size={5} color="#f59e0b" />}>
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Heading level={2} icon={<Icon icon={Flag} size={5} color="#f59e0b" />}>
|
||||
Next Race
|
||||
</Heading>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
<Surface variant="muted" rounded="lg" border padding={4} style={{ background: 'rgba(245, 158, 11, 0.05)', borderColor: 'rgba(245, 158, 11, 0.2)' }}>
|
||||
<SharedStack direction="row" align="center" justify="between">
|
||||
<SharedStack direction="row" align="center" gap={4}>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Surface variant="muted" rounded="lg" padding={3} style={{ background: 'rgba(245, 158, 11, 0.1)' }}>
|
||||
<SharedIcon icon={Flag} size={6} color="#f59e0b" />
|
||||
<Icon icon={Flag} size={6} color="#f59e0b" />
|
||||
</Surface>
|
||||
<SharedBox>
|
||||
<SharedText size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</SharedText>
|
||||
<SharedText size="sm" color="text-gray-400" block mt={1}>{league.nextRace.formattedDate}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
<SharedButton variant="secondary">
|
||||
<Box>
|
||||
<Text size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</Text>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>{league.nextRace.formattedDate}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Button variant="secondary">
|
||||
View Schedule
|
||||
</SharedButton>
|
||||
</SharedStack>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Surface>
|
||||
</SharedCard>
|
||||
</Card>
|
||||
</GridItem>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{activeTab === 'drivers' && (
|
||||
<SharedCard p={0}>
|
||||
<SharedBox p={4} borderBottom borderColor="border-neutral-800">
|
||||
<Card p={0}>
|
||||
<Box p={4} borderBottom borderColor="border-neutral-800">
|
||||
<Heading level={2}>Championship Standings</Heading>
|
||||
<SharedText size="sm" color="text-gray-400" block mt={1}>Top drivers carrying sponsor branding</SharedText>
|
||||
</SharedBox>
|
||||
<SharedStack gap={0}>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>Top drivers carrying sponsor branding</Text>
|
||||
</Box>
|
||||
<Stack gap={0}>
|
||||
{viewData.drivers.map((driver, index) => (
|
||||
<SharedBox key={driver.id} p={4} borderBottom={index < viewData.drivers.length - 1} borderColor="border-neutral-800/50">
|
||||
<SharedStack direction="row" align="center" justify="between">
|
||||
<SharedStack direction="row" align="center" gap={4}>
|
||||
<Box key={driver.id} p={4} borderBottom={index < viewData.drivers.length - 1} borderColor="border-neutral-800/50">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#262626' }}>
|
||||
<SharedText weight="bold" color="text-white">{driver.positionLabel}</SharedText>
|
||||
<Text weight="bold" color="text-white">{driver.positionLabel}</Text>
|
||||
</Surface>
|
||||
<SharedBox>
|
||||
<SharedText weight="medium" color="text-white" block>{driver.name}</SharedText>
|
||||
<SharedText size="sm" color="text-gray-500" block mt={1}>{driver.team} • {driver.country}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
<SharedStack direction="row" align="center" gap={8}>
|
||||
<SharedBox textAlign="right">
|
||||
<SharedText weight="medium" color="text-white" block>{driver.formattedRaces}</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500">races</SharedText>
|
||||
</SharedBox>
|
||||
<SharedBox textAlign="right">
|
||||
<SharedText weight="semibold" color="text-white" block>{driver.formattedImpressions}</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500">views</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<Box>
|
||||
<Text weight="medium" color="text-white" block>{driver.name}</Text>
|
||||
<Text size="sm" color="text-gray-500" block mt={1}>{driver.team} • {driver.country}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" gap={8}>
|
||||
<Box textAlign="right">
|
||||
<Text weight="medium" color="text-white" block>{driver.formattedRaces}</Text>
|
||||
<Text size="xs" color="text-gray-500">races</Text>
|
||||
</Box>
|
||||
<Box textAlign="right">
|
||||
<Text weight="semibold" color="text-white" block>{driver.formattedImpressions}</Text>
|
||||
<Text size="xs" color="text-gray-500">views</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</SharedStack>
|
||||
</SharedCard>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'races' && (
|
||||
<SharedCard p={0}>
|
||||
<SharedBox p={4} borderBottom borderColor="border-neutral-800">
|
||||
<Card p={0}>
|
||||
<Box p={4} borderBottom borderColor="border-neutral-800">
|
||||
<Heading level={2}>Race Calendar</Heading>
|
||||
<SharedText size="sm" color="text-gray-400" block mt={1}>Season schedule with view statistics</SharedText>
|
||||
</SharedBox>
|
||||
<SharedStack gap={0}>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>Season schedule with view statistics</Text>
|
||||
</Box>
|
||||
<Stack gap={0}>
|
||||
{viewData.races.map((race, index) => (
|
||||
<SharedBox key={race.id} p={4} borderBottom={index < viewData.races.length - 1} borderColor="border-neutral-800/50">
|
||||
<SharedStack direction="row" align="center" justify="between">
|
||||
<SharedStack direction="row" align="center" gap={4}>
|
||||
<SharedBox w="3" h="3" rounded="full" bg={race.status === 'completed' ? 'bg-performance-green' : 'bg-warning-amber'} />
|
||||
<SharedBox>
|
||||
<SharedText weight="medium" color="text-white" block>{race.name}</SharedText>
|
||||
<SharedText size="sm" color="text-gray-500" block mt={1}>{race.formattedDate}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
<SharedBox>
|
||||
<Box key={race.id} p={4} borderBottom={index < viewData.races.length - 1} borderColor="border-neutral-800/50">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Box w="3" h="3" rounded="full" bg={race.status === 'completed' ? 'bg-performance-green' : 'bg-warning-amber'} />
|
||||
<Box>
|
||||
<Text weight="medium" color="text-white" block>{race.name}</Text>
|
||||
<Text size="sm" color="text-gray-500" block mt={1}>{race.formattedDate}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box>
|
||||
{race.status === 'completed' ? (
|
||||
<SharedBox textAlign="right">
|
||||
<SharedText weight="semibold" color="text-white" block>{race.formattedViews}</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500">views</SharedText>
|
||||
</SharedBox>
|
||||
<Box textAlign="right">
|
||||
<Text weight="semibold" color="text-white" block>{race.formattedViews}</Text>
|
||||
<Text size="xs" color="text-gray-500">views</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<SponsorStatusChip status="pending" label="Upcoming" />
|
||||
)}
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</SharedStack>
|
||||
</SharedCard>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'sponsor' && (
|
||||
@@ -383,62 +381,62 @@ export function SponsorLeagueDetailTemplate({
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={{ base: 12, lg: 4 }}>
|
||||
<SharedStack gap={6}>
|
||||
<Stack gap={6}>
|
||||
<SponsorBrandingPreview
|
||||
name="Your Brand"
|
||||
/>
|
||||
|
||||
<SharedCard>
|
||||
<SharedBox mb={4}>
|
||||
<Heading level={2} icon={<SharedIcon icon={CreditCard} size={5} color="#3b82f6" />}>
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Heading level={2} icon={<Icon icon={CreditCard} size={5} color="#3b82f6" />}>
|
||||
Sponsorship Summary
|
||||
</Heading>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
|
||||
<SharedStack gap={3} mb={6}>
|
||||
<Stack gap={3} mb={6}>
|
||||
<InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} />
|
||||
<InfoRow label="Season Price" value={selectedTier === 'main' ? league.sponsorSlots.main.priceLabel : league.sponsorSlots.secondary.priceLabel} />
|
||||
<InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} />
|
||||
<SharedBox pt={4} borderTop borderColor="border-neutral-800">
|
||||
<SharedStack direction="row" align="center" justify="between">
|
||||
<SharedText weight="semibold" color="text-white">Total (excl. VAT)</SharedText>
|
||||
<SharedText size="xl" weight="bold" color="text-white">
|
||||
<Box pt={4} borderTop borderColor="border-neutral-800">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text weight="semibold" color="text-white">Total (excl. VAT)</Text>
|
||||
<Text size="xl" weight="bold" color="text-white">
|
||||
${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * (1 + siteConfig.fees.platformFeePercent / 100)).toFixed(2)}
|
||||
</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<SharedText size="xs" color="text-gray-500" block mb={4}>
|
||||
<Text size="xs" color="text-gray-500" block mb={4}>
|
||||
{siteConfig.vat.notice}
|
||||
</SharedText>
|
||||
</Text>
|
||||
|
||||
<SharedStack direction="row" gap={3}>
|
||||
<SharedButton variant="primary" fullWidth icon={<SharedIcon icon={Megaphone} size={4} />}>
|
||||
<Stack direction="row" gap={3}>
|
||||
<Button variant="primary" fullWidth icon={<Icon icon={Megaphone} size={4} />}>
|
||||
Request Sponsorship
|
||||
</SharedButton>
|
||||
<SharedButton variant="secondary" icon={<SharedIcon icon={FileText} size={4} />}>
|
||||
</Button>
|
||||
<Button variant="secondary" icon={<Icon icon={FileText} size={4} />}>
|
||||
Download Info Pack
|
||||
</SharedButton>
|
||||
</SharedStack>
|
||||
</SharedCard>
|
||||
</SharedStack>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
)}
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value, color = 'text-white', last }: { label: string, value: string | number, color?: string, last?: boolean }) {
|
||||
return (
|
||||
<SharedBox py={2} borderBottom={!last} borderColor="border-neutral-800/50">
|
||||
<SharedStack direction="row" align="center" justify="between">
|
||||
<SharedText color="text-gray-400">{label}</SharedText>
|
||||
<SharedText weight="medium" color={color}>{value}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<Box py={2} borderBottom={!last} borderColor="border-neutral-800/50">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text color="text-gray-400">{label}</Text>
|
||||
<Text weight="medium" color={color}>{value}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,11 @@ import { StewardingQueuePanel } from '@/components/leagues/StewardingQueuePanel'
|
||||
import { StewardingStats } from '@/components/leagues/StewardingStats';
|
||||
import { PenaltyFAB } from '@/components/races/PenaltyFAB';
|
||||
import type { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedCard
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
|
||||
interface StewardingTemplateProps extends TemplateProps<StewardingViewData> {
|
||||
@@ -51,7 +49,7 @@ export function StewardingTemplate({
|
||||
currentDriverId,
|
||||
}: StewardingTemplateProps) {
|
||||
return (
|
||||
<SharedStack gap={6}>
|
||||
<Stack gap={6}>
|
||||
<StewardingStats
|
||||
totalPending={viewData.totalPending}
|
||||
totalResolved={viewData.totalResolved}
|
||||
@@ -59,41 +57,41 @@ export function StewardingTemplate({
|
||||
/>
|
||||
|
||||
{/* Tab navigation */}
|
||||
<SharedBox borderBottom borderColor="border-charcoal-outline">
|
||||
<SharedStack direction="row" gap={4}>
|
||||
<SharedBox
|
||||
<Box borderBottom borderColor="border-charcoal-outline">
|
||||
<Stack direction="row" gap={4}>
|
||||
<Box
|
||||
borderBottom={activeTab === 'pending'}
|
||||
borderColor={activeTab === 'pending' ? 'border-primary-blue' : undefined}
|
||||
>
|
||||
<SharedButton
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onTabChange('pending')}
|
||||
rounded={false}
|
||||
>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<SharedText weight="medium" color={activeTab === 'pending' ? 'text-primary-blue' : undefined}>Pending Protests</SharedText>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Text weight="medium" color={activeTab === 'pending' ? 'text-primary-blue' : undefined}>Pending Protests</Text>
|
||||
{viewData.totalPending > 0 && (
|
||||
<SharedBox px={2} py={0.5} fontSize="0.75rem" bg="bg-warning-amber/20" color="text-warning-amber" rounded="full">
|
||||
<Box px={2} py={0.5} fontSize="0.75rem" bg="bg-warning-amber/20" color="text-warning-amber" rounded="full">
|
||||
{viewData.totalPending}
|
||||
</SharedBox>
|
||||
</Box>
|
||||
)}
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
<SharedBox
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
borderBottom={activeTab === 'history'}
|
||||
borderColor={activeTab === 'history' ? 'border-primary-blue' : undefined}
|
||||
>
|
||||
<SharedButton
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onTabChange('history')}
|
||||
rounded={false}
|
||||
>
|
||||
<SharedText weight="medium" color={activeTab === 'history' ? 'text-primary-blue' : undefined}>History</SharedText>
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<Text weight="medium" color={activeTab === 'history' ? 'text-primary-blue' : undefined}>History</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
{activeTab === 'pending' ? (
|
||||
@@ -102,15 +100,15 @@ export function StewardingTemplate({
|
||||
onReview={onReviewProtest}
|
||||
/>
|
||||
) : (
|
||||
<SharedCard>
|
||||
<SharedBox p={6}>
|
||||
<Card>
|
||||
<Box p={6}>
|
||||
<PenaltyHistoryList
|
||||
protests={allResolvedProtests}
|
||||
races={racesMap}
|
||||
drivers={driverMap}
|
||||
/>
|
||||
</SharedBox>
|
||||
</SharedCard>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
@@ -141,6 +139,6 @@ export function StewardingTemplate({
|
||||
races={viewData.races.map(r => ({ id: r.id, track: r.track, scheduledAt: new Date(r.scheduledAt) }))}
|
||||
/>
|
||||
)}
|
||||
</SharedStack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
/>
|
||||
<Group justify="end" fullWidth>
|
||||
<Link href={routes.auth.forgotPassword}>
|
||||
<Text size="xs" color="text-primary-accent">
|
||||
<Text size="xs" variant="primary">
|
||||
Forgot password?
|
||||
</Text>
|
||||
</Link>
|
||||
@@ -101,10 +101,10 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
|
||||
{viewData.hasInsufficientPermissions && (
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={AlertCircle} size={5} color="var(--color-warning)" />
|
||||
<Icon icon={AlertCircle} size={5} color="var(--ui-color-intent-warning)" />
|
||||
<Group direction="column" gap={1}>
|
||||
<Text weight="bold" color="text-warning-amber" block size="sm">Insufficient Permissions</Text>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
<Text weight="bold" variant="warning" block size="sm">Insufficient Permissions</Text>
|
||||
<Text size="xs" variant="low" block>
|
||||
Please log in with an account that has the required role.
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -133,17 +133,17 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
</AuthForm>
|
||||
|
||||
<AuthFooterLinks>
|
||||
<Text size="sm" color="text-gray-400">
|
||||
<Text size="sm" variant="low">
|
||||
Don't have an account?{' '}
|
||||
<Link
|
||||
href={viewData.returnTo && viewData.returnTo !== '/dashboard' ? `/auth/signup?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/signup'}
|
||||
>
|
||||
<Text as="span" color="text-primary-accent" weight="bold">Create one</Text>
|
||||
<Text as="span" variant="primary" weight="bold">Create one</Text>
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Group direction="column" gap={1} align="center" fullWidth>
|
||||
<Text size="xs" color="text-gray-600">
|
||||
<Text size="xs" variant="low">
|
||||
By signing in, you agree to our{' '}
|
||||
<Link href="/terms">Terms</Link>
|
||||
{' '}and{' '}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
<AuthForm onSubmit={formActions.handleSubmit}>
|
||||
<Group direction="column" gap={6} fullWidth>
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Personal Information</Text>
|
||||
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wide" block>Personal Information</Text>
|
||||
<Grid cols={{ base: 1, md: 2 }} gap={4}>
|
||||
<Input
|
||||
label="First Name"
|
||||
@@ -91,9 +91,9 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Grid>
|
||||
|
||||
<Group direction="row" align="start" gap={2} fullWidth>
|
||||
<Icon icon={AlertCircle} size={3.5} color="var(--color-warning)" />
|
||||
<Text size="xs" color="text-med">
|
||||
<Text weight="bold" color="text-warning-amber">Note:</Text> Your name cannot be changed after signup.
|
||||
<Icon icon={AlertCircle} size={3.5} color="var(--ui-color-intent-warning)" />
|
||||
<Text size="xs" variant="low">
|
||||
<Text weight="bold" variant="warning">Note:</Text> Your name cannot be changed after signup.
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -113,7 +113,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Group>
|
||||
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Security</Text>
|
||||
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wide" block>Security</Text>
|
||||
<PasswordField
|
||||
label="Password"
|
||||
id="password"
|
||||
@@ -138,15 +138,15 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
<Text size="xs" weight="bold" color="text-low" uppercase>
|
||||
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||
{passwordStrength.label}
|
||||
</Text>
|
||||
</Group>
|
||||
<Grid cols={2} gap={2}>
|
||||
{passwordRequirements.map((req, index) => (
|
||||
<Group key={index} direction="row" align="center" gap={1.5}>
|
||||
<Icon icon={req.met ? Check : X} size={3} color={req.met ? 'var(--color-success)' : 'var(--color-text-low)'} />
|
||||
<Text size="xs" color={req.met ? 'text-med' : 'text-low'}>
|
||||
<Icon icon={req.met ? Check : X} size={3} color={req.met ? 'var(--ui-color-intent-success)' : 'var(--ui-color-text-low)'} />
|
||||
<Text size="xs" variant={req.met ? 'med' : 'low'}>
|
||||
{req.label}
|
||||
</Text>
|
||||
</Group>
|
||||
@@ -173,8 +173,8 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
|
||||
{mutationState.error && (
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
|
||||
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--ui-color-intent-critical)" />
|
||||
<Text size="sm" variant="critical">{mutationState.error}</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -190,17 +190,17 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</AuthForm>
|
||||
|
||||
<AuthFooterLinks>
|
||||
<Text size="sm" color="text-gray-400">
|
||||
<Text size="sm" variant="low">
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
href={viewData.returnTo && viewData.returnTo !== '/onboarding' ? `/auth/login?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/login'}
|
||||
>
|
||||
<Text color="text-primary-accent" weight="bold">Sign in</Text>
|
||||
<Text variant="primary" weight="bold">Sign in</Text>
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Group direction="column" gap={1} align="center" fullWidth>
|
||||
<Text size="xs" color="text-gray-600">
|
||||
<Text size="xs" variant="low">
|
||||
By creating an account, you agree to our{' '}
|
||||
<Link href="/terms">Terms</Link>
|
||||
{' '}and{' '}
|
||||
|
||||
@@ -15,8 +15,8 @@ export function GlobalFooterTemplate(_props: GlobalFooterViewData) {
|
||||
<Stack colSpan={{ base: 1, md: 2 }} gap={6}>
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<BrandMark />
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="[#23272B]" pl={4}>
|
||||
<Box w="4px" h="4px" rounded="full" bg="primary-accent" animate="pulse" />
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="var(--ui-color-border-default)" pl={4}>
|
||||
<Box w="4px" h="4px" rounded="full" bg="var(--ui-color-intent-primary)" animate="pulse" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
|
||||
INFRASTRUCTURE
|
||||
</Text>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function GlobalSidebarTemplate(_props: GlobalSidebarViewData) {
|
||||
<DashboardRail>
|
||||
<Box py={6} fullWidth>
|
||||
<Box px={6} mb={8}>
|
||||
<Text size="xs" color="text-gray-500" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
NAVIGATION
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -20,9 +20,9 @@ export function HeaderContentTemplate(_props: HeaderContentViewData) {
|
||||
<>
|
||||
<Stack direction="row" align="center" gap={6}>
|
||||
<BrandMark href={homeHref} priority />
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="[#23272B]" pl={6}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="primary-accent" animate="pulse" />
|
||||
<Text size="xs" color="text-gray-500" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="var(--ui-color-border-default)" pl={6}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-primary)" animate="pulse" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
MOTORSPORT INFRASTRUCTURE
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -35,9 +35,9 @@ export function HeaderContentTemplate(_props: HeaderContentViewData) {
|
||||
)}
|
||||
|
||||
<Box display="flex" alignItems="center" gap={4}>
|
||||
<Stack direction="row" display={{ base: 'none', md: 'flex' }} align="center" gap={1} px={3} py={1} border borderColor="[#23272B]" bg="[#141619]/20">
|
||||
<Text size="xs" color="text-gray-600" weight="bold" font="mono">STATUS:</Text>
|
||||
<Text size="xs" color="text-success-green" weight="bold" font="mono">OPERATIONAL</Text>
|
||||
<Stack direction="row" display={{ base: 'none', md: 'flex' }} align="center" gap={1} px={3} py={1} border borderColor="var(--ui-color-border-default)" bg="var(--ui-color-bg-surface-muted)">
|
||||
<Text size="xs" variant="low" weight="bold" font="mono">STATUS:</Text>
|
||||
<Text size="xs" variant="success" weight="bold" font="mono">OPERATIONAL</Text>
|
||||
</Stack>
|
||||
<HeaderActions isAuthenticated={isAuthenticated} />
|
||||
</Box>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { SharedContainer, SharedStack, SharedText } from '@/components/shared/UIComponents';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
|
||||
@@ -11,12 +13,12 @@ interface ErrorTemplateProps extends TemplateProps<ViewData> {
|
||||
|
||||
export function ErrorTemplate({ message = "An error occurred", description = "Please try again later" }: ErrorTemplateProps) {
|
||||
return (
|
||||
<SharedContainer size="lg">
|
||||
<SharedStack align="center" gap={4} py={12}>
|
||||
<SharedText color="text-red-400">{message}</SharedText>
|
||||
<SharedText color="text-gray-400">{description}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedContainer>
|
||||
<Container size="lg">
|
||||
<Stack align="center" gap={4} py={12}>
|
||||
<Text color="text-red-400">{message}</Text>
|
||||
<Text color="text-gray-400">{description}</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,11 +29,11 @@ interface EmptyTemplateProps extends TemplateProps<ViewData> {
|
||||
|
||||
export function EmptyTemplate({ title, description }: EmptyTemplateProps) {
|
||||
return (
|
||||
<SharedContainer size="lg">
|
||||
<SharedStack align="center" gap={2} py={12}>
|
||||
<SharedText size="xl" weight="semibold" color="text-white">{title}</SharedText>
|
||||
<SharedText color="text-gray-400">{description}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedContainer>
|
||||
<Container size="lg">
|
||||
<Stack align="center" gap={2} py={12}>
|
||||
<Text size="xl" weight="semibold" color="text-white">{title}</Text>
|
||||
<Text color="text-gray-400">{description}</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user