website refactor
This commit is contained in:
124
apps/website/components/races/ProtestCard.tsx
Normal file
124
apps/website/components/races/ProtestCard.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { AlertCircle, AlertTriangle, Video } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
interface Protest {
|
||||
id: string;
|
||||
status: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
filedAt: string;
|
||||
incident: {
|
||||
lap: number;
|
||||
description: string;
|
||||
};
|
||||
proofVideoUrl?: string;
|
||||
decisionNotes?: string;
|
||||
}
|
||||
|
||||
interface Driver {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ProtestCardProps {
|
||||
protest: Protest;
|
||||
protester?: Driver;
|
||||
accused?: Driver;
|
||||
isAdmin: boolean;
|
||||
onReview: (id: string) => void;
|
||||
formatDate: (date: string) => string;
|
||||
}
|
||||
|
||||
export function ProtestCard({ protest, protester, accused, isAdmin, onReview, formatDate }: ProtestCardProps) {
|
||||
const daysSinceFiled = Math.floor(
|
||||
(Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const isUrgent = daysSinceFiled > 2 && protest.status === 'pending';
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const variants: Record<string, { bg: string, text: string, label: string }> = {
|
||||
pending: { bg: 'rgba(245, 158, 11, 0.2)', text: '#f59e0b', label: 'Pending' },
|
||||
under_review: { bg: 'rgba(245, 158, 11, 0.2)', text: '#f59e0b', label: 'Pending' },
|
||||
upheld: { bg: 'rgba(239, 68, 68, 0.2)', text: '#ef4444', label: 'Upheld' },
|
||||
dismissed: { bg: 'rgba(115, 115, 115, 0.2)', text: '#9ca3af', label: 'Dismissed' },
|
||||
withdrawn: { bg: 'rgba(59, 130, 246, 0.2)', text: '#3b82f6', label: 'Withdrawn' },
|
||||
};
|
||||
const config = variants[status] || variants.pending;
|
||||
return (
|
||||
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: config.bg, paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
|
||||
<Text size="xs" weight="medium" style={{ color: config.text }}>{config.label}</Text>
|
||||
</Surface>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ borderLeft: isUrgent ? '4px solid #ef4444' : undefined }}>
|
||||
<Stack direction="row" align="start" justify="between" gap={4}>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack direction="row" align="center" gap={2} mb={2} wrap>
|
||||
<Icon icon={AlertCircle} size={4} color="#9ca3af" />
|
||||
<Link href={`/drivers/${protest.protestingDriverId}`}>
|
||||
<Text weight="medium" color="text-white">{protester?.name || 'Unknown'}</Text>
|
||||
</Link>
|
||||
<Text size="sm" color="text-gray-500">vs</Text>
|
||||
<Link href={`/drivers/${protest.accusedDriverId}`}>
|
||||
<Text weight="medium" color="text-white">{accused?.name || 'Unknown'}</Text>
|
||||
</Link>
|
||||
{getStatusBadge(protest.status)}
|
||||
{isUrgent && (
|
||||
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: 'rgba(239, 68, 68, 0.2)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
|
||||
<Stack direction="row" align="center" gap={1}>
|
||||
<Icon icon={AlertTriangle} size={3} color="#ef4444" />
|
||||
<Text size="xs" weight="medium" color="text-error-red">{daysSinceFiled}d old</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" gap={4} mb={2} wrap>
|
||||
<Text size="sm" color="text-gray-400">Lap {protest.incident.lap}</Text>
|
||||
<Text size="sm" color="text-gray-400">•</Text>
|
||||
<Text size="sm" color="text-gray-400">Filed {formatDate(protest.filedAt)}</Text>
|
||||
{protest.proofVideoUrl && (
|
||||
<>
|
||||
<Text size="sm" color="text-gray-400">•</Text>
|
||||
<Link href={protest.proofVideoUrl} target="_blank">
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Icon icon={Video} size={3.5} color="#3b82f6" />
|
||||
<Text size="sm" color="text-primary-blue">Video Evidence</Text>
|
||||
</Stack>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Text size="sm" color="text-gray-300" block>{protest.incident.description}</Text>
|
||||
|
||||
{protest.decisionNotes && (
|
||||
<Box mt={4} p={3} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', borderRadius: '0.5rem', border: '1px solid #262626' }}>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block mb={1}>Steward Decision</Text>
|
||||
<Text size="sm" color="text-gray-300">{protest.decisionNotes}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{isAdmin && protest.status === 'pending' && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => onReview(protest.id)}
|
||||
size="sm"
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user