Files
gridpilot.gg/apps/website/components/sponsors/SponsorInsightsCard.tsx
2026-01-17 22:55:03 +01:00

312 lines
11 KiB
TypeScript

'use client';
import React, { useCallback, useState } from 'react';
import {
Activity,
Check,
Loader2,
MessageCircle,
Shield,
Target,
Users,
Zap,
Calendar,
LucideIcon
} from 'lucide-react';
const ICON_MAP: Record<string, LucideIcon> = {
users: Users,
zap: Zap,
calendar: Calendar,
activity: Activity,
shield: Shield,
target: Target,
message: MessageCircle,
};
import { Button } from '@/ui/Button';
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 { SponsorMetricCard } from '@/ui/SponsorMetricCard';
import { SponsorSlotCard } from '@/ui/SponsorSlotCard';
import { SponsorshipTierBadge } from '@/ui/SponsorshipTierBadge';
import { InfoBox } from '@/ui/InfoBox';
import { SponsorMetric, SponsorshipSlot } from './SponsorInsightsCardTypes';
import { getTierStyles, getEntityLabel, getSponsorshipTagline } from './SponsorInsightsCardHelpers';
export type EntityType = 'league' | 'race' | 'driver' | 'team';
export interface SponsorInsightsProps {
entityType: EntityType;
entityId: string;
entityName: string;
tier: 'premium' | 'standard' | 'starter';
metrics: SponsorMetric[];
slots: SponsorshipSlot[];
additionalStats?: {
label: string;
items: Array<{ label: string; value: string | number }>;
};
trustScore?: number;
discordMembers?: number;
monthlyActivity?: number;
ctaLabel?: string;
ctaHref?: string;
currentSponsorId?: string;
onSponsorshipRequested?: (tier: 'main' | 'secondary') => void;
onNavigate: (href: string) => void;
}
export function SponsorInsightsCard({
entityType,
entityId,
entityName,
tier,
metrics,
slots,
additionalStats,
trustScore,
discordMembers,
monthlyActivity,
ctaLabel,
ctaHref,
currentSponsorId,
onSponsorshipRequested,
onNavigate,
}: SponsorInsightsProps) {
const tierStyles = getTierStyles(tier);
const [applyingTier, setApplyingTier] = useState<'main' | 'secondary' | null>(null);
const [appliedTiers, setAppliedTiers] = useState<Set<'main' | 'secondary'>>(new Set());
const [error, setError] = useState<string | null>(null);
const mainSlot = slots.find(s => s.tier === 'main');
const secondarySlots = slots.filter(s => s.tier === 'secondary');
const availableSecondary = secondarySlots.filter(s => s.available).length;
const getSponsorableEntityType = useCallback((type: EntityType): 'driver' | 'team' | 'race' | 'season' => {
switch (type) {
case 'league': return 'season';
case 'race': return 'race';
case 'driver': return 'driver';
case 'team': return 'team';
}
}, []);
const handleSponsorClick = useCallback(async (slotTier: 'main' | 'secondary') => {
if (!currentSponsorId) {
const href = ctaHref || `/sponsor/${entityType}s/${entityId}?tier=${slotTier}`;
onNavigate(href);
return;
}
if (appliedTiers.has(slotTier)) {
onNavigate(`/sponsor/dashboard`);
return;
}
setApplyingTier(slotTier);
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]));
onSponsorshipRequested?.(slotTier);
} catch (err) {
console.error('Failed to apply for sponsorship:', err);
setError(err instanceof Error ? err.message : 'Failed to submit sponsorship request');
} finally {
setApplyingTier(null);
}
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, onNavigate, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
return (
<Card
mb={6}
borderColor="border-primary-blue/30"
bg={`linear-gradient(to right, ${tierStyles.gradient.split(' ')[1]}, ${tierStyles.gradient.split(' ')[2]})`}
>
<Box display="flex" alignItems="start" justifyContent="between" mb={4}>
<Box>
<Stack direction="row" align="center" gap={2} mb={1}>
<Icon icon={Target} size={5} color="rgb(59, 130, 246)" />
<Heading level={3}>Sponsorship Opportunity</Heading>
</Stack>
<Text size="sm" color="text-gray-400">
{getSponsorshipTagline(entityType)}
</Text>
</Box>
<SponsorshipTierBadge tier={tier} entityLabel={getEntityLabel(entityType)} />
</Box>
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={3} mb={4}>
{metrics.slice(0, 4).map((metric, index) => {
const IconComponent = typeof metric.icon === 'string' ? ICON_MAP[metric.icon] || Target : metric.icon;
return (
<SponsorMetricCard
key={index}
label={metric.label}
value={metric.value}
icon={IconComponent as LucideIcon}
color={metric.color}
trend={metric.trend}
/>
);
})}
</Box>
{(trustScore !== undefined || discordMembers !== undefined || monthlyActivity !== undefined) && (
<Box display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
{trustScore !== undefined && (
<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>
</Stack>
)}
{discordMembers !== undefined && (
<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>
</Stack>
)}
{monthlyActivity !== undefined && (
<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>
</Stack>
)}
</Box>
)}
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={3} mb={4}>
{mainSlot && (
<SponsorSlotCard
variant="main"
title="Main Sponsor Slot"
status={mainSlot.available ? 'Available' : 'Taken'}
statusColor={mainSlot.available ? 'text-performance-green' : 'text-gray-500'}
benefits={mainSlot.benefits.join(' • ')}
available={mainSlot.available}
price={`$${mainSlot.price.toLocaleString()}/season`}
action={
<Button
variant="primary"
onClick={() => handleSponsorClick('main')}
disabled={applyingTier === 'main'}
size="sm"
>
{applyingTier === 'main' ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Loader2} size={3} animate="spin" />
Applying...
</Stack>
) : appliedTiers.has('main') ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Check} size={3} />
Applied
</Stack>
) : (
'Apply to Sponsor'
)}
</Button>
}
/>
)}
{secondarySlots.length > 0 && (
<SponsorSlotCard
variant="secondary"
title="Secondary Slots"
status={`${availableSecondary}/${secondarySlots.length} Available`}
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`}
action={
<Button
variant="secondary"
onClick={() => handleSponsorClick('secondary')}
disabled={applyingTier === 'secondary'}
size="sm"
>
{applyingTier === 'secondary' ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Loader2} size={3} animate="spin" />
Applying...
</Stack>
) : appliedTiers.has('secondary') ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Check} size={3} />
Applied
</Stack>
) : (
'Apply to Sponsor'
)}
</Button>
}
/>
)}
</Box>
{additionalStats && (
<Box mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
<Heading level={4} mb={2} color="text-gray-400">{additionalStats.label}</Heading>
<Box display="flex" flexWrap="wrap" gap={4}>
{additionalStats.items.map((item, index) => (
<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}
</Text>
</Stack>
))}
</Box>
</Box>
)}
{error && (
<InfoBox
variant="warning"
icon={Target}
title="Error"
description={error}
/>
)}
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-charcoal-outline/50">
<Text size="xs" color="text-gray-500">
10% platform fee applies Logos burned on all liveries Sponsorships are attached to seasons
{appliedTiers.size > 0 && ' • Application pending review'}
</Text>
<Button
variant="secondary"
onClick={() => onNavigate(ctaHref || `/sponsor/${entityType}s/${entityId}`)}
size="sm"
>
{ctaLabel || 'View Full Details'}
</Button>
</Box>
</Card>
);
}