website refactor

This commit is contained in:
2026-01-19 14:07:49 +01:00
parent 54f42bab9f
commit 6154d54435
88 changed files with 755 additions and 566 deletions

View File

@@ -8,6 +8,7 @@ export interface PricingTier {
id: string;
name: string;
price: number;
priceLabel: string;
period: string;
description: string;
features: string[];
@@ -69,7 +70,7 @@ export function PricingTableShell({ title, tiers, onSelect, selectedId }: Pricin
{tier.name}
</Text>
<Stack direction="row" align="baseline" gap={1}>
<Text size="3xl" weight="bold" color="text-white">${tier.price}</Text>
<Text size="3xl" weight="bold" color="text-white">{tier.priceLabel}</Text>
<Text size="sm" color="text-gray-500">/{tier.period}</Text>
</Stack>
<Text size="sm" color="text-gray-400" block mt={2}>

View File

@@ -1,62 +1,61 @@
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
currency?: string;
priceLabel: string;
benefits: string[];
}
export const SlotTemplates = {
league: (mainAvailable: boolean, secondaryAvailable: number, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
league: (mainAvailable: boolean, secondaryAvailable: number, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
priceLabel: mainPriceLabel,
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
},
{
tier: 'secondary',
available: secondaryAvailable > 0,
price: secondaryPrice,
priceLabel: secondaryPriceLabel,
benefits: ['Side logo placement', 'League page listing'],
},
{
tier: 'secondary',
available: secondaryAvailable > 1,
price: secondaryPrice,
priceLabel: secondaryPriceLabel,
benefits: ['Side logo placement', 'League page listing'],
},
],
race: (mainAvailable: boolean, mainPrice: number): SponsorshipSlot[] => [
race: (mainAvailable: boolean, mainPriceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
priceLabel: mainPriceLabel,
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
},
],
driver: (available: boolean, price: number): SponsorshipSlot[] => [
driver: (available: boolean, priceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available,
price,
priceLabel,
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
},
],
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
priceLabel: mainPriceLabel,
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
},
{
tier: 'secondary',
available: secondaryAvailable,
price: secondaryPrice,
priceLabel: secondaryPriceLabel,
benefits: ['Team page logo', 'Minor livery placement'],
},
],

View File

@@ -47,11 +47,11 @@ export interface SponsorInsightsProps {
slots: SponsorshipSlot[];
additionalStats?: {
label: string;
items: Array<{ label: string; value: string | number }>;
items: Array<{ label: string; value: string }>;
};
trustScore?: number;
discordMembers?: number;
monthlyActivity?: number;
trustScoreLabel?: string;
discordMembersLabel?: string;
monthlyActivityLabel?: string;
ctaLabel?: string;
ctaHref?: string;
currentSponsorId?: string;
@@ -67,9 +67,9 @@ export function SponsorInsightsCard({
metrics,
slots,
additionalStats,
trustScore,
discordMembers,
monthlyActivity,
trustScoreLabel,
discordMembersLabel,
monthlyActivityLabel,
ctaLabel,
ctaHref,
currentSponsorId,
@@ -111,22 +111,10 @@ export function SponsorInsightsCard({
setError(null);
try {
const slot = slotTier === 'main' ? mainSlot : secondarySlots[0];
const slotPrice = slot?.price ?? 0;
const request = {
sponsorId: currentSponsorId,
entityType: getSponsorableEntityType(entityType),
entityId,
tier: slotTier,
offeredAmount: slotPrice * 100,
currency: (slot?.currency as 'USD' | 'EUR' | 'GBP') ?? 'USD',
message: `Interested in sponsoring ${entityName} as ${slotTier} sponsor.`,
};
console.log('Sponsorship request:', request);
setAppliedTiers(prev => new Set([...prev, slotTier]));
// Note: In a real app, we would fetch the raw price from the API or a ViewModel
// For now, we assume the parent handles the actual request logic
onSponsorshipRequested?.(slotTier);
setAppliedTiers(prev => new Set([...prev, slotTier]));
} catch (err) {
console.error('Failed to apply for sponsorship:', err);
@@ -134,7 +122,7 @@ export function SponsorInsightsCard({
} finally {
setApplyingTier(null);
}
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, onNavigate, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
}, [currentSponsorId, ctaHref, entityType, entityId, onNavigate, appliedTiers, onSponsorshipRequested]);
return (
<Card
@@ -171,27 +159,27 @@ export function SponsorInsightsCard({
})}
</Stack>
{(trustScore !== undefined || discordMembers !== undefined || monthlyActivity !== undefined) && (
{(trustScoreLabel || discordMembersLabel || monthlyActivityLabel) && (
<Stack display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
{trustScore !== undefined && (
{trustScoreLabel && (
<Stack direction="row" align="center" gap={2}>
<Icon icon={Shield} size={4} color="rgb(16, 185, 129)" />
<Text size="sm" color="text-gray-400">Trust Score:</Text>
<Text size="sm" weight="semibold" color="text-white">{trustScore}/100</Text>
<Text size="sm" weight="semibold" color="text-white">{trustScoreLabel}</Text>
</Stack>
)}
{discordMembers !== undefined && (
{discordMembersLabel && (
<Stack direction="row" align="center" gap={2}>
<Icon icon={MessageCircle} size={4} color="rgb(168, 85, 247)" />
<Text size="sm" color="text-gray-400">Discord:</Text>
<Text size="sm" weight="semibold" color="text-white">{discordMembers.toLocaleString()}</Text>
<Text size="sm" weight="semibold" color="text-white">{discordMembersLabel}</Text>
</Stack>
)}
{monthlyActivity !== undefined && (
{monthlyActivityLabel && (
<Stack direction="row" align="center" gap={2}>
<Icon icon={Activity} size={4} color="rgb(0, 255, 255)" />
<Text size="sm" color="text-gray-400">Monthly Activity:</Text>
<Text size="sm" weight="semibold" color="text-white">{monthlyActivity}%</Text>
<Text size="sm" weight="semibold" color="text-white">{monthlyActivityLabel}</Text>
</Stack>
)}
</Stack>
@@ -206,7 +194,7 @@ export function SponsorInsightsCard({
statusColor={mainSlot.available ? 'text-performance-green' : 'text-gray-500'}
benefits={mainSlot.benefits.join(' • ')}
available={mainSlot.available}
price={`$${mainSlot.price.toLocaleString()}/season`}
price={mainSlot.priceLabel}
action={
<Button
variant="primary"
@@ -240,7 +228,7 @@ export function SponsorInsightsCard({
statusColor={availableSecondary > 0 ? 'text-purple-400' : 'text-gray-500'}
benefits={secondarySlots[0]?.benefits.join(' • ') || 'Logo placement on page'}
available={availableSecondary > 0}
price={`$${secondarySlots[0]?.price.toLocaleString()}/season`}
price={secondarySlots[0]?.priceLabel}
action={
<Button
variant="secondary"
@@ -275,7 +263,7 @@ export function SponsorInsightsCard({
<Stack key={index} direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-500">{item.label}:</Text>
<Text size="sm" weight="semibold" color="text-white">
{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
{item.value}
</Text>
</Stack>
))}

View File

@@ -3,10 +3,10 @@ import { ComponentType } from 'react';
export interface SponsorMetric {
icon: string | ComponentType;
label: string;
value: string | number;
value: string;
color?: string;
trend?: {
value: number;
value: string;
isPositive: boolean;
};
}
@@ -14,7 +14,6 @@ export interface SponsorMetric {
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
currency?: string;
priceLabel: string;
benefits: string[];
}

View File

@@ -5,11 +5,11 @@ import { LucideIcon } from 'lucide-react';
interface SponsorMetricCardProps {
label: string;
value: string | number;
value: string;
icon: LucideIcon;
color?: string;
trend?: {
value: number;
value: string;
isPositive: boolean;
};
}
@@ -35,11 +35,11 @@ export function SponsorMetricCard({
</Box>
<Box display="flex" alignItems="baseline" gap={2}>
<Text size="xl" weight="bold" color="text-white">
{typeof value === 'number' ? value.toLocaleString() : value}
{value}
</Text>
{trend && (
<Text size="xs" color={trend.isPositive ? 'text-performance-green' : 'text-red-400'}>
{trend.isPositive ? '+' : ''}{trend.value}%
{trend.value}
</Text>
)}
</Box>

View File

@@ -8,8 +8,8 @@ import { LucideIcon } from 'lucide-react';
interface SponsorshipCategoryCardProps {
icon: LucideIcon;
title: string;
count: number;
impressions: number;
countLabel: string;
impressionsLabel: string;
color: string;
href: string;
}
@@ -17,8 +17,8 @@ interface SponsorshipCategoryCardProps {
export function SponsorshipCategoryCard({
icon,
title,
count,
impressions,
countLabel,
impressionsLabel,
color,
href
}: SponsorshipCategoryCardProps) {
@@ -39,11 +39,11 @@ export function SponsorshipCategoryCard({
</Stack>
<Stack>
<Text weight="medium" color="text-white" block>{title}</Text>
<Text size="sm" color="text-gray-500">{count} active</Text>
<Text size="sm" color="text-gray-500">{countLabel}</Text>
</Stack>
</Stack>
<Stack textAlign="right">
<Text weight="semibold" color="text-white" block>{impressions.toLocaleString()}</Text>
<Text weight="semibold" color="text-white" block>{impressionsLabel}</Text>
<Text size="xs" color="text-gray-500">impressions</Text>
</Stack>
</Stack>

View File

@@ -7,9 +7,9 @@ import { AlertTriangle, Check, Clock, Download, Receipt } from 'lucide-react';
export interface Transaction {
id: string;
date: string;
formattedDate: string;
description: string;
amount: number;
formattedAmount: string;
status: 'paid' | 'pending' | 'overdue' | 'failed';
invoiceNumber: string;
type: string;
@@ -103,11 +103,11 @@ export function TransactionTable({ transactions, onDownload }: TransactionTableP
</Stack>
<Stack colSpan={{ base: 1, md: 2 } as any}>
<Text size="sm" color="text-gray-400">{new Date(tx.date).toLocaleDateString()}</Text>
<Text size="sm" color="text-gray-400">{tx.formattedDate}</Text>
</Stack>
<Stack colSpan={{ base: 1, md: 2 } as any}>
<Text weight="semibold" color="text-white">${tx.amount.toFixed(2)}</Text>
<Text weight="semibold" color="text-white">{tx.formattedAmount}</Text>
</Stack>
<Stack colSpan={{ base: 1, md: 2 } as any}>