website refactor

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

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Box, BoxProps } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { LucideIcon } from 'lucide-react';
interface BillingStatProps {
label: string;
value: string | number;
subValue?: string;
icon: LucideIcon;
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
}
function BillingStat({ label, value, subValue, icon, variant = 'default' }: BillingStatProps) {
const colorMap = {
default: 'text-white',
success: 'text-performance-green',
warning: 'text-warning-amber',
danger: 'text-racing-red',
info: 'text-primary-blue',
};
const bgMap = {
default: 'bg-iron-gray/50',
success: 'bg-performance-green/10',
warning: 'bg-warning-amber/10',
danger: 'bg-racing-red/10',
info: 'bg-primary-blue/10',
};
return (
<Box p={4} bg="bg-iron-gray/20" rounded="lg" border borderColor="border-charcoal-outline/50">
<Stack direction="row" align="center" gap={3} mb={2}>
<Box p={2} rounded="md" bg={bgMap[variant] as BoxProps<'div'>['bg']}>
<Icon icon={icon} size={4} color={colorMap[variant] as any} />
</Box>
<Text size="xs" weight="medium" color="text-gray-500" uppercase letterSpacing="wider">
{label}
</Text>
</Stack>
<Box>
<Text size="xl" weight="bold" color={colorMap[variant] as any}>
{value}
</Text>
{subValue && (
<Text size="xs" color="text-gray-500" block mt={0.5}>
{subValue}
</Text>
)}
</Box>
</Box>
);
}
interface BillingSummaryPanelProps {
stats: BillingStatProps[];
}
/**
* BillingSummaryPanel
*
* A semantic panel for displaying billing metrics.
* Dense, grid-based layout for financial data.
*/
export function BillingSummaryPanel({ stats }: BillingSummaryPanelProps) {
return (
<Box display="grid" gridCols={{ base: 1, sm: 2, lg: 4 }} gap={4} mb={8}>
{stats.map((stat, index) => (
<BillingStat key={index} {...stat} />
))}
</Box>
);
}

View File

@@ -0,0 +1,104 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { Check, Info } from 'lucide-react';
export interface PricingTier {
id: string;
name: string;
price: number;
period: string;
description: string;
features: string[];
isPopular?: boolean;
available?: boolean;
}
interface PricingTableShellProps {
title: string;
tiers: PricingTier[];
onSelect?: (id: string) => void;
selectedId?: string;
}
/**
* PricingTableShell
*
* A semantic component for displaying sponsorship pricing tiers.
* Clean, comparison-focused layout.
*/
export function PricingTableShell({ title, tiers, onSelect, selectedId }: PricingTableShellProps) {
return (
<Box mb={8}>
<Heading level={3} fontSize="lg" weight="semibold" mb={4} color="text-white">
{title}
</Heading>
<Box display="grid" gridCols={{ base: 1, md: 3 }} gap={6}>
{tiers.map((tier) => (
<Box
key={tier.id}
p={6}
rounded="xl"
border
borderColor={selectedId === tier.id ? 'border-primary-blue' : 'border-charcoal-outline/50'}
bg={selectedId === tier.id ? 'bg-primary-blue/5' : 'bg-iron-gray/10'}
position="relative"
cursor="pointer"
onClick={() => onSelect?.(tier.id)}
transition-all
hoverBorderColor={selectedId === tier.id ? 'border-primary-blue' : 'border-charcoal-outline'}
>
{tier.isPopular && (
<Box
position="absolute"
top={0}
right={6}
transform="-translate-y-1/2"
bg="bg-primary-blue"
px={3}
py={1}
rounded="full"
>
<Text size="xs" weight="bold" color="text-white" uppercase letterSpacing="wider">Popular</Text>
</Box>
)}
<Box mb={6}>
<Text size="sm" weight="bold" color="text-primary-blue" uppercase letterSpacing="wider" block mb={2}>
{tier.name}
</Text>
<Stack direction="row" align="baseline" gap={1}>
<Text size="3xl" weight="bold" color="text-white">${tier.price}</Text>
<Text size="sm" color="text-gray-500">/{tier.period}</Text>
</Stack>
<Text size="sm" color="text-gray-400" block mt={2}>
{tier.description}
</Text>
</Box>
<Stack gap={3} mb={8}>
{tier.features.map((feature, i) => (
<Stack key={i} direction="row" align="start" gap={3}>
<Box mt={0.5}>
<Icon icon={Check} size={3.5} color="text-performance-green" />
</Box>
<Text size="sm" color="text-gray-300">{feature}</Text>
</Stack>
))}
</Stack>
{!tier.available && (
<Box display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-racing-red/10" border borderColor="border-racing-red/20">
<Icon icon={Info} size={4} color="text-racing-red" />
<Text size="xs" weight="medium" color="text-racing-red">Currently Unavailable</Text>
</Box>
)}
</Box>
))}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { Box, BoxProps } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { LucideIcon, Clock } from 'lucide-react';
export interface Activity {
id: string;
type: 'sponsorship_approved' | 'payment_received' | 'new_opportunity' | 'contract_expiring';
title: string;
description: string;
timestamp: string;
icon: LucideIcon;
color: string;
}
interface SponsorActivityPanelProps {
activities: Activity[];
}
/**
* SponsorActivityPanel
*
* A semantic component for displaying a feed of sponsor activities.
* Dense, chronological list.
*/
export function SponsorActivityPanel({ activities }: SponsorActivityPanelProps) {
return (
<Box>
<Heading level={3} fontSize="lg" weight="semibold" mb={4} color="text-white">
Recent Activity
</Heading>
<Box border rounded="xl" borderColor="border-charcoal-outline/50" overflow="hidden" bg="bg-iron-gray/10">
{activities.length === 0 ? (
<Box p={8} textAlign="center">
<Icon icon={Clock} size={8} color="text-gray-600" className="mx-auto mb-3" />
<Text color="text-gray-500">No recent activity to show.</Text>
</Box>
) : (
<Box>
{activities.map((activity, index) => (
<Box
key={activity.id}
p={4}
display="flex"
gap={4}
borderBottom={index !== activities.length - 1}
borderColor="border-charcoal-outline/30"
hoverBg="bg-iron-gray/20"
transition-colors
>
<Box
w="10"
h="10"
rounded="lg"
display="flex"
alignItems="center"
justifyContent="center"
bg={activity.color.replace('text-', 'bg-').concat('/10') as BoxProps<'div'>['bg']}
>
<Icon icon={activity.icon} size={5} color={activity.color as any} />
</Box>
<Box flexGrow={1}>
<Box display="flex" alignItems="center" justifyContent="between" mb={0.5}>
<Text weight="medium" color="text-white">{activity.title}</Text>
<Text size="xs" color="text-gray-500">{activity.timestamp}</Text>
</Box>
<Text size="sm" color="text-gray-400">{activity.description}</Text>
</Box>
</Box>
))}
</Box>
)}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,85 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { SponsorLogo } from '@/ui/SponsorLogo';
interface SponsorBrandingPreviewProps {
name: string;
logoUrl?: string;
primaryColor?: string;
secondaryColor?: string;
}
/**
* SponsorBrandingPreview
*
* Visualizes how a sponsor's branding (logo and colors) will appear on the platform.
*/
export function SponsorBrandingPreview({
name,
logoUrl,
primaryColor = '#198CFF',
secondaryColor = '#141619'
}: SponsorBrandingPreviewProps) {
return (
<Card p={0} overflow="hidden">
<Box p={4} borderBottom borderColor="border-charcoal-outline">
<Text size="xs" weight="bold" uppercase letterSpacing="wider" color="text-gray-400">
Branding Preview
</Text>
</Box>
<Box p={6}>
<Stack gap={6}>
{/* Logo Preview */}
<Stack align="center" gap={4}>
<Surface
variant="muted"
rounded="xl"
padding={8}
bg="bg-iron-gray/10"
border
borderColor="border-charcoal-outline/50"
w="full"
display="flex"
alignItems="center"
justifyContent="center"
>
<SponsorLogo src={logoUrl} alt={name} size={64} />
</Surface>
<Text size="sm" color="text-gray-400">Primary Logo Asset</Text>
</Stack>
{/* Color Palette */}
<Box>
<Text size="xs" weight="bold" uppercase letterSpacing="wider" color="text-gray-500" block mb={3}>
Color Palette
</Text>
<Stack direction="row" gap={4}>
<Stack gap={2} flexGrow={1}>
<Box h={12} rounded="lg" backgroundColor={primaryColor} border borderColor="border-white/10" />
<Text size="xs" color="text-gray-400" textAlign="center">{primaryColor}</Text>
</Stack>
<Stack gap={2} flexGrow={1}>
<Box h={12} rounded="lg" backgroundColor={secondaryColor} border borderColor="border-white/10" />
<Text size="xs" color="text-gray-400" textAlign="center">{secondaryColor}</Text>
</Stack>
</Stack>
</Box>
{/* Mockup Hint */}
<Surface variant="muted" rounded="lg" padding={3} bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
<Text size="xs" color="text-primary-blue" textAlign="center">
These assets will be used for broadcast overlays and car liveries.
</Text>
</Surface>
</Stack>
</Box>
</Card>
);
}

View File

@@ -0,0 +1,144 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Button } from '@/ui/Button';
import {
Trophy,
Users,
Car,
Flag,
Megaphone,
ChevronRight,
ExternalLink,
Calendar,
BarChart3
} from 'lucide-react';
import { SponsorStatusChip, SponsorStatus } from './SponsorStatusChip';
export type SponsorshipType = 'league' | 'team' | 'driver' | 'race' | 'platform';
interface SponsorContractCardProps {
id: string;
type: SponsorshipType;
status: string; // Accept raw status string
title: string;
subtitle?: string;
tier: string;
investment: string;
impressions: string;
startDate?: string;
endDate?: string;
onViewDetails?: (id: string) => void;
}
const TYPE_CONFIG = {
league: { icon: Trophy, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', label: 'League' },
team: { icon: Users, color: 'text-purple-400', bgColor: 'bg-purple-400/10', label: 'Team' },
driver: { icon: Car, color: 'text-performance-green', bgColor: 'bg-performance-green/10', label: 'Driver' },
race: { icon: Flag, color: 'text-warning-amber', bgColor: 'bg-warning-amber/10', label: 'Race' },
platform: { icon: Megaphone, color: 'text-racing-red', bgColor: 'bg-racing-red/10', label: 'Platform' },
};
function mapStatus(status: string): SponsorStatus {
if (status === 'pending_approval' || status === 'pending') return 'pending';
if (status === 'rejected' || status === 'declined') return 'declined';
if (status === 'expired') return 'expired';
if (status === 'approved') return 'approved';
if (status === 'active') return 'active';
return 'pending';
}
/**
* SponsorContractCard
*
* Semantic component for displaying a sponsorship contract/campaign.
* Provides a high-density overview of the sponsorship status and performance.
*/
export function SponsorContractCard({
id,
type,
status,
title,
subtitle,
tier,
investment,
impressions,
startDate,
endDate,
onViewDetails
}: SponsorContractCardProps) {
const typeConfig = TYPE_CONFIG[type];
const mappedStatus = mapStatus(status);
return (
<Card p={5} hoverBorderColor="border-primary-blue/30">
<Stack direction="row" align="start" justify="between" mb={4}>
<Stack direction="row" align="center" gap={3}>
<Box w="10" h="10" rounded="lg" bg={typeConfig.bgColor as string} display="flex" alignItems="center" justifyContent="center">
<Icon icon={typeConfig.icon} size={5} color={typeConfig.color as string} />
</Box>
<Box>
<Text size="xs" weight="bold" uppercase letterSpacing="wider" color={typeConfig.color as string}>
{typeConfig.label} {tier}
</Text>
<Heading level={4} fontSize="lg" weight="bold" color="text-white">
{title}
</Heading>
{subtitle && <Text size="xs" color="text-gray-500">{subtitle}</Text>}
</Box>
</Stack>
<SponsorStatusChip status={mappedStatus} />
</Stack>
<Box display="grid" gridCols={3} gap={4} mb={4} p={3} rounded="lg" bg="bg-iron-gray/20">
<Box>
<Text size="xs" color="text-gray-500" block mb={1}>Impressions</Text>
<Stack direction="row" align="center" gap={1}>
<Icon icon={BarChart3} size={3} color="text-gray-400" />
<Text weight="bold" color="text-white">{impressions}</Text>
</Stack>
</Box>
<Box>
<Text size="xs" color="text-gray-500" block mb={1}>Investment</Text>
<Text weight="bold" color="text-white">{investment}</Text>
</Box>
<Box>
<Text size="xs" color="text-gray-500" block mb={1}>Term</Text>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Calendar} size={3} color="text-gray-400" />
<Text weight="bold" color="text-white">{endDate || 'N/A'}</Text>
</Stack>
</Box>
</Box>
<Stack direction="row" align="center" justify="between" pt={4} borderTop borderColor="border-charcoal-outline/30">
<Text size="xs" color="text-gray-500">
{startDate ? `Started ${startDate}` : 'Contract pending'}
</Text>
<Stack direction="row" gap={2}>
<Button
variant="secondary"
size="sm"
icon={<Icon icon={ExternalLink} size={3} />}
onClick={() => onViewDetails?.(id)}
>
View
</Button>
<Button
variant="secondary"
size="sm"
icon={<Icon icon={ChevronRight} size={3} />}
>
Details
</Button>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,100 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import {
LayoutDashboard,
RefreshCw,
Settings,
Bell
} from 'lucide-react';
import { Link } from '@/ui/Link';
import { routes } from '@/lib/routing/RouteConfig';
interface SponsorDashboardHeaderProps {
sponsorName: string;
onRefresh?: () => void;
}
/**
* SponsorDashboardHeader
*
* Semantic header for the sponsor dashboard.
* Orchestrates dashboard-level actions and identity.
*/
export function SponsorDashboardHeader({ sponsorName, onRefresh }: SponsorDashboardHeaderProps) {
return (
<Box mb={8}>
<Stack direction={{ base: 'col', md: 'row' }} align="start" justify="between" gap={4}>
<Stack direction="row" align="center" gap={4}>
<Surface
variant="muted"
rounded="lg"
padding={3}
bg="bg-primary-blue/10"
border
borderColor="border-primary-blue/20"
>
<Icon icon={LayoutDashboard} size={6} color="text-primary-blue" />
</Surface>
<Box>
<Heading level={1} fontSize="2xl" weight="bold" color="text-white">
Sponsor Dashboard
</Heading>
<Text color="text-gray-400" size="sm">
Welcome back, <Text color="text-white" weight="medium">{sponsorName}</Text>
</Text>
</Box>
</Stack>
<Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={1} bg="bg-iron-gray/10" border borderColor="border-charcoal-outline/30">
<Stack direction="row" align="center">
{(['7d', '30d', '90d'] as const).map((range) => (
<Button
key={range}
variant="ghost"
size="sm"
px={3}
>
{range}
</Button>
))}
</Stack>
</Surface>
<Button variant="secondary" size="sm" onClick={onRefresh} icon={<Icon icon={RefreshCw} size={4} />}>
Refresh
</Button>
<Link href={routes.sponsor.settings}>
<Button variant="secondary" size="sm" icon={<Icon icon={Settings} size={4} />}>
Settings
</Button>
</Link>
<Button variant="secondary" size="sm" position="relative">
<Icon icon={Bell} size={4} />
<Box
position="absolute"
top={-1}
right={-1}
w={2}
h={2}
rounded="full"
bg="bg-racing-red"
border
borderColor="border-deep-graphite"
/>
</Button>
</Stack>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
interface SponsorHeaderPanelProps {
icon: LucideIcon;
title: string;
description?: string;
actions?: React.ReactNode;
stats?: React.ReactNode;
}
/**
* SponsorHeaderPanel
*
* A semantic header panel for sponsor-related pages.
* Follows the finance/ops panel aesthetic with dense information.
*/
export function SponsorHeaderPanel({
icon,
title,
description,
actions,
stats,
}: SponsorHeaderPanelProps) {
return (
<Box mb={8}>
<Stack direction="row" align="start" justify="between" wrap gap={6}>
<Stack direction="row" align="center" gap={4}>
<Surface
variant="muted"
rounded="xl"
border
padding={3}
bg="bg-gradient-to-br"
borderColor="border-primary-blue/20"
>
<Icon icon={icon} size={7} color="text-primary-blue" />
</Surface>
<Box>
<Heading level={1} fontSize="2xl" weight="bold">{title}</Heading>
{description && (
<Text color="text-gray-400" block mt={1} size="sm">{description}</Text>
)}
</Box>
</Stack>
<Stack direction="row" align="center" gap={4}>
{stats && (
<Box borderRight borderColor="border-charcoal-outline" pr={4} mr={2} display={{ base: 'none', md: 'block' }}>
{stats}
</Box>
)}
{actions && <Box>{actions}</Box>}
</Stack>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,98 @@
'use client';
import React from 'react';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
import { Stack } from '@/ui/Stack';
import {
Clock,
CheckCircle2,
AlertCircle,
ArrowUpRight,
DollarSign
} from 'lucide-react';
export interface PayoutItem {
id: string;
date: string;
amount: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
recipient: string;
description: string;
}
interface SponsorPayoutQueueTableProps {
payouts: PayoutItem[];
}
const STATUS_CONFIG = {
pending: { icon: Clock, color: 'text-warning-amber', label: 'Pending' },
processing: { icon: ArrowUpRight, color: 'text-primary-blue', label: 'Processing' },
completed: { icon: CheckCircle2, color: 'text-performance-green', label: 'Completed' },
failed: { icon: AlertCircle, color: 'text-racing-red', label: 'Failed' },
};
/**
* SponsorPayoutQueueTable
*
* High-density table for tracking sponsorship payouts and financial transactions.
*/
export function SponsorPayoutQueueTable({ payouts }: SponsorPayoutQueueTableProps) {
return (
<Table>
<TableHead>
<TableRow>
<TableHeader>Date</TableHeader>
<TableHeader>Recipient</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader textAlign="right">Amount</TableHeader>
<TableHeader textAlign="center">Status</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{payouts.map((payout) => {
const status = STATUS_CONFIG[payout.status];
return (
<TableRow key={payout.id}>
<TableCell>
<Text size="sm" color="text-gray-300">{payout.date}</Text>
</TableCell>
<TableCell>
<Text size="sm" weight="medium" color="text-white">{payout.recipient}</Text>
</TableCell>
<TableCell>
<Text size="sm" color="text-gray-400">{payout.description}</Text>
</TableCell>
<TableCell textAlign="right">
<Stack direction="row" align="center" justify="end" gap={1}>
<Icon icon={DollarSign} size={3} color="text-gray-500" />
<Text size="sm" weight="bold" color="text-white">{payout.amount}</Text>
</Stack>
</TableCell>
<TableCell>
<Box display="flex" justifyContent="center">
<Badge variant={payout.status === 'completed' ? 'success' : payout.status === 'failed' ? 'danger' : 'warning'}>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={status.icon} size={3} />
<Text size="xs" weight="bold" uppercase letterSpacing="wider">{status.label}</Text>
</Stack>
</Badge>
</Box>
</TableCell>
</TableRow>
);
})}
{payouts.length === 0 && (
<TableRow>
<TableCell colSpan={5} textAlign="center" py={12}>
<Text color="text-gray-500">No payouts in queue</Text>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import {
Check,
Clock,
ThumbsUp,
ThumbsDown,
XCircle,
AlertTriangle
} from 'lucide-react';
export type SponsorStatus = 'active' | 'pending' | 'approved' | 'declined' | 'expired' | 'warning';
interface SponsorStatusChipProps {
status: SponsorStatus;
label?: string;
}
const STATUS_CONFIG = {
active: { icon: Check, color: 'text-performance-green', bgColor: 'bg-performance-green/10', label: 'Active' },
pending: { icon: Clock, color: 'text-warning-amber', bgColor: 'bg-warning-amber/10', label: 'Pending' },
approved: { icon: ThumbsUp, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', label: 'Approved' },
declined: { icon: ThumbsDown, color: 'text-racing-red', bgColor: 'bg-racing-red/10', label: 'Declined' },
expired: { icon: XCircle, color: 'text-gray-400', bgColor: 'bg-gray-400/10', label: 'Expired' },
warning: { icon: AlertTriangle, color: 'text-warning-amber', bgColor: 'bg-warning-amber/10', label: 'Action Required' },
};
/**
* SponsorStatusChip
*
* Semantic status indicator for sponsorship states.
* Follows the "Precision Racing Minimal" theme with instrument-grade feedback.
*/
export function SponsorStatusChip({ status, label }: SponsorStatusChipProps) {
const config = STATUS_CONFIG[status];
return (
<Box
px={2.5}
py={1}
rounded="full"
bg={config.bgColor as string}
border
borderColor="border-charcoal-outline/30"
display="inline-block"
>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={config.icon} size={3} color={config.color as string} />
<Text
size="xs"
weight="bold"
color={config.color as string}
uppercase
letterSpacing="wider"
>
{label || config.label}
</Text>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,138 @@
import React from 'react';
import { Box, BoxProps } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Button } from '@/ui/Button';
import { Download, Receipt, Clock, Check, AlertTriangle } from 'lucide-react';
export interface Transaction {
id: string;
date: string;
description: string;
amount: number;
status: 'paid' | 'pending' | 'overdue' | 'failed';
invoiceNumber: string;
type: string;
}
interface TransactionTableProps {
transactions: Transaction[];
onDownload?: (id: string) => void;
}
const STATUS_CONFIG = {
paid: {
icon: Check,
label: 'Paid',
color: 'text-performance-green',
bg: 'bg-performance-green/10',
border: 'border-performance-green/30'
},
pending: {
icon: Clock,
label: 'Pending',
color: 'text-warning-amber',
bg: 'bg-warning-amber/10',
border: 'border-warning-amber/30'
},
overdue: {
icon: AlertTriangle,
label: 'Overdue',
color: 'text-racing-red',
bg: 'bg-racing-red/10',
border: 'border-racing-red/30'
},
failed: {
icon: AlertTriangle,
label: 'Failed',
color: 'text-racing-red',
bg: 'bg-racing-red/10',
border: 'border-racing-red/30'
},
};
/**
* TransactionTable
*
* A semantic table for displaying financial transactions.
* Dense layout with thin dividers, optimized for ops panels.
*/
export function TransactionTable({ transactions, onDownload }: TransactionTableProps) {
return (
<Box border rounded="xl" borderColor="border-charcoal-outline/50" overflow="hidden" bg="bg-iron-gray/10">
<Box display={{ base: 'none', md: 'grid' }} gridCols={12} gap={4} p={4} bg="bg-iron-gray/30" borderBottom borderColor="border-charcoal-outline/50">
<Box colSpan={5}>
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">Description</Text>
</Box>
<Box colSpan={2}>
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">Date</Text>
</Box>
<Box colSpan={2}>
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">Amount</Text>
</Box>
<Box colSpan={2}>
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">Status</Text>
</Box>
<Box colSpan={1} textAlign="right">
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">Action</Text>
</Box>
</Box>
<Box>
{transactions.map((tx, index) => {
const status = STATUS_CONFIG[tx.status];
return (
<Box
key={tx.id}
display="grid"
gridCols={{ base: 1, md: 12 }}
gap={4}
p={4}
alignItems="center"
borderBottom={index !== transactions.length - 1}
borderColor="border-charcoal-outline/30"
hoverBg="bg-iron-gray/20"
transition-colors
>
<Box colSpan={{ base: 1, md: 5 }} display="flex" alignItems="center" gap={3}>
<Box w="8" h="8" rounded="lg" bg="bg-iron-gray/50" display="flex" alignItems="center" justifyContent="center">
<Icon icon={Receipt} size={4} color="text-gray-400" />
</Box>
<Box>
<Text weight="medium" color="text-white" block>{tx.description}</Text>
<Text size="xs" color="text-gray-500">{tx.invoiceNumber} {tx.type}</Text>
</Box>
</Box>
<Box colSpan={{ base: 1, md: 2 }}>
<Text size="sm" color="text-gray-400">{new Date(tx.date).toLocaleDateString()}</Text>
</Box>
<Box colSpan={{ base: 1, md: 2 }}>
<Text weight="semibold" color="text-white">${tx.amount.toFixed(2)}</Text>
</Box>
<Box colSpan={{ base: 1, md: 2 }}>
<Box display="inline-flex" alignItems="center" gap={1.5} px={2} py={0.5} rounded="full" bg={status.bg as BoxProps<'div'>['bg']} border borderColor={status.border as BoxProps<'div'>['borderColor']}>
<Icon icon={status.icon} size={3} color={status.color as any} />
<Text size="xs" weight="medium" color={status.color as any}>{status.label}</Text>
</Box>
</Box>
<Box colSpan={{ base: 1, md: 1 }} textAlign="right">
<Button
variant="ghost"
size="sm"
onClick={() => onDownload?.(tx.id)}
icon={<Icon icon={Download} size={3} />}
>
PDF
</Button>
</Box>
</Box>
);
})}
</Box>
</Box>
);
}