website refactor
This commit is contained in:
75
apps/website/components/sponsors/BillingSummaryPanel.tsx
Normal file
75
apps/website/components/sponsors/BillingSummaryPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
104
apps/website/components/sponsors/PricingTableShell.tsx
Normal file
104
apps/website/components/sponsors/PricingTableShell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
78
apps/website/components/sponsors/SponsorActivityPanel.tsx
Normal file
78
apps/website/components/sponsors/SponsorActivityPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
85
apps/website/components/sponsors/SponsorBrandingPreview.tsx
Normal file
85
apps/website/components/sponsors/SponsorBrandingPreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
144
apps/website/components/sponsors/SponsorContractCard.tsx
Normal file
144
apps/website/components/sponsors/SponsorContractCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
100
apps/website/components/sponsors/SponsorDashboardHeader.tsx
Normal file
100
apps/website/components/sponsors/SponsorDashboardHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
apps/website/components/sponsors/SponsorHeaderPanel.tsx
Normal file
64
apps/website/components/sponsors/SponsorHeaderPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
98
apps/website/components/sponsors/SponsorPayoutQueueTable.tsx
Normal file
98
apps/website/components/sponsors/SponsorPayoutQueueTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
66
apps/website/components/sponsors/SponsorStatusChip.tsx
Normal file
66
apps/website/components/sponsors/SponsorStatusChip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
138
apps/website/components/sponsors/TransactionTable.tsx
Normal file
138
apps/website/components/sponsors/TransactionTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user