299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { SponsorMetricCard } from '@/components/sponsors/SponsorMetricCard';
|
|
import { SponsorshipTierBadge } from '@/components/sponsors/SponsorshipTierBadge';
|
|
import { SponsorSlotCard } from '@/components/sponsors/SponsorSlotCard';
|
|
import { Button } from '@/ui/Button';
|
|
import { Card } from '@/ui/Card';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { Icon } from '@/ui/Icon';
|
|
import { InfoBox } from '@/ui/InfoBox';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import {
|
|
Activity,
|
|
Calendar,
|
|
Check,
|
|
Loader2,
|
|
LucideIcon,
|
|
MessageCircle,
|
|
Shield,
|
|
Target,
|
|
Users,
|
|
Zap
|
|
} from 'lucide-react';
|
|
import { useCallback, useState } from 'react';
|
|
import { getEntityLabel, getSponsorshipTagline, getTierStyles } from './SponsorInsightsCardHelpers';
|
|
import { SponsorMetric, SponsorshipSlot } from './SponsorInsightsCardTypes';
|
|
|
|
const ICON_MAP: Record<string, LucideIcon> = {
|
|
users: Users,
|
|
zap: Zap,
|
|
calendar: Calendar,
|
|
activity: Activity,
|
|
shield: Shield,
|
|
target: Target,
|
|
message: MessageCircle,
|
|
};
|
|
|
|
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 }>;
|
|
};
|
|
trustScoreLabel?: string;
|
|
discordMembersLabel?: string;
|
|
monthlyActivityLabel?: string;
|
|
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,
|
|
trustScoreLabel,
|
|
discordMembersLabel,
|
|
monthlyActivityLabel,
|
|
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 {
|
|
// 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);
|
|
setError(err instanceof Error ? err.message : 'Failed to submit sponsorship request');
|
|
} finally {
|
|
setApplyingTier(null);
|
|
}
|
|
}, [currentSponsorId, ctaHref, entityType, entityId, onNavigate, appliedTiers, onSponsorshipRequested]);
|
|
|
|
return (
|
|
<Card
|
|
mb={6}
|
|
borderColor="border-primary-blue/30"
|
|
bg={`linear-gradient(to right, ${tierStyles.gradient.split(' ')[1]}, ${tierStyles.gradient.split(' ')[2]})`}
|
|
>
|
|
<Stack display="flex" alignItems="start" justifyContent="between" mb={4}>
|
|
<Stack>
|
|
<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>
|
|
</Stack>
|
|
<SponsorshipTierBadge tier={tier} entityLabel={getEntityLabel(entityType)} />
|
|
</Stack>
|
|
|
|
<Stack 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}
|
|
/>
|
|
);
|
|
})}
|
|
</Stack>
|
|
|
|
{(trustScoreLabel || discordMembersLabel || monthlyActivityLabel) && (
|
|
<Stack display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
|
|
{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">{trustScoreLabel}</Text>
|
|
</Stack>
|
|
)}
|
|
{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">{discordMembersLabel}</Text>
|
|
</Stack>
|
|
)}
|
|
{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">{monthlyActivityLabel}</Text>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
|
|
<Stack 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.priceLabel}
|
|
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]?.priceLabel}
|
|
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>
|
|
}
|
|
/>
|
|
)}
|
|
</Stack>
|
|
|
|
{additionalStats && (
|
|
<Stack mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
|
|
<Heading level={4} mb={2} color="text-gray-400">{additionalStats.label}</Heading>
|
|
<Stack 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">
|
|
{item.value}
|
|
</Text>
|
|
</Stack>
|
|
))}
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
|
|
{error && (
|
|
<InfoBox
|
|
variant="warning"
|
|
icon={Target}
|
|
title="Error"
|
|
description={error}
|
|
/>
|
|
)}
|
|
|
|
<Stack 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>
|
|
</Stack>
|
|
</Card>
|
|
);
|
|
}
|