website refactor
This commit is contained in:
@@ -1,70 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/ui/Button';
|
||||
import Card from '@/ui/Card';
|
||||
import { useInject } from '@/lib/di/hooks/useInject';
|
||||
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Activity,
|
||||
Check,
|
||||
Loader2,
|
||||
MessageCircle,
|
||||
Shield,
|
||||
Target
|
||||
Target,
|
||||
LucideIcon
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
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, getEntityIcon, getSponsorshipTagline } from './SponsorInsightsCardHelpers';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
import { getTierStyles, getEntityLabel, getSponsorshipTagline } from './SponsorInsightsCardHelpers';
|
||||
|
||||
export type EntityType = 'league' | 'race' | 'driver' | 'team';
|
||||
|
||||
export interface SponsorInsightsProps {
|
||||
// Entity info
|
||||
entityType: EntityType;
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
|
||||
// Tier classification
|
||||
tier: 'premium' | 'standard' | 'starter';
|
||||
|
||||
// Key metrics (shown in grid)
|
||||
metrics: SponsorMetric[];
|
||||
|
||||
// Sponsorship availability
|
||||
slots: SponsorshipSlot[];
|
||||
|
||||
// Optional: additional stats section
|
||||
additionalStats?: {
|
||||
label: string;
|
||||
items: Array<{ label: string; value: string | number }>;
|
||||
};
|
||||
|
||||
// Optional: trust indicators
|
||||
trustScore?: number;
|
||||
discordMembers?: number;
|
||||
monthlyActivity?: number;
|
||||
|
||||
// CTA customization
|
||||
ctaLabel?: string;
|
||||
ctaHref?: string;
|
||||
|
||||
// Optional: current sponsor ID (if logged in as sponsor)
|
||||
currentSponsorId?: string;
|
||||
|
||||
// Optional: callback when sponsorship request is submitted
|
||||
onSponsorshipRequested?: (tier: 'main' | 'secondary') => void;
|
||||
onNavigate: (href: string) => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export default function SponsorInsightsCard({
|
||||
export function SponsorInsightsCard({
|
||||
entityType,
|
||||
entityId,
|
||||
entityName,
|
||||
@@ -79,14 +62,10 @@ export default function SponsorInsightsCard({
|
||||
ctaHref,
|
||||
currentSponsorId,
|
||||
onSponsorshipRequested,
|
||||
onNavigate,
|
||||
}: SponsorInsightsProps) {
|
||||
// TODO components should not fetch any data
|
||||
const router = useRouter();
|
||||
const sponsorshipService = useInject(SPONSOR_SERVICE_TOKEN);
|
||||
const tierStyles = getTierStyles(tier);
|
||||
const EntityIcon = getEntityIcon(entityType);
|
||||
|
||||
// State for sponsorship application
|
||||
const [applyingTier, setApplyingTier] = useState<'main' | 'secondary' | null>(null);
|
||||
const [appliedTiers, setAppliedTiers] = useState<Set<'main' | 'secondary'>>(new Set());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -95,10 +74,9 @@ export default function SponsorInsightsCard({
|
||||
const secondarySlots = slots.filter(s => s.tier === 'secondary');
|
||||
const availableSecondary = secondarySlots.filter(s => s.available).length;
|
||||
|
||||
// Map EntityType to SponsorableEntityType
|
||||
const getSponsorableEntityType = useCallback((type: EntityType): 'driver' | 'team' | 'race' | 'season' => {
|
||||
switch (type) {
|
||||
case 'league': return 'season'; // Leagues are sponsored via their seasons
|
||||
case 'league': return 'season';
|
||||
case 'race': return 'race';
|
||||
case 'driver': return 'driver';
|
||||
case 'team': return 'team';
|
||||
@@ -106,20 +84,17 @@ export default function SponsorInsightsCard({
|
||||
}, []);
|
||||
|
||||
const handleSponsorClick = useCallback(async (slotTier: 'main' | 'secondary') => {
|
||||
// If no sponsor ID, redirect to sponsor signup/login
|
||||
if (!currentSponsorId) {
|
||||
const href = ctaHref || `/sponsor/${entityType}s/${entityId}?tier=${slotTier}`;
|
||||
router.push(href);
|
||||
onNavigate(href);
|
||||
return;
|
||||
}
|
||||
|
||||
// If already applied for this tier, show details page
|
||||
if (appliedTiers.has(slotTier)) {
|
||||
router.push(`/sponsor/dashboard`);
|
||||
onNavigate(`/sponsor/dashboard`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply for sponsorship using service
|
||||
setApplyingTier(slotTier);
|
||||
setError(null);
|
||||
|
||||
@@ -127,26 +102,18 @@ export default function SponsorInsightsCard({
|
||||
const slot = slotTier === 'main' ? mainSlot : secondarySlots[0];
|
||||
const slotPrice = slot?.price ?? 0;
|
||||
|
||||
// Note: The sponsorship service would need a method to submit sponsorship requests
|
||||
// For now, we'll use a placeholder since the exact API may not be available
|
||||
const request = {
|
||||
sponsorId: currentSponsorId,
|
||||
entityType: getSponsorableEntityType(entityType),
|
||||
entityId,
|
||||
tier: slotTier,
|
||||
offeredAmount: slotPrice * 100, // Convert to cents
|
||||
offeredAmount: slotPrice * 100,
|
||||
currency: (slot?.currency as 'USD' | 'EUR' | 'GBP') ?? 'USD',
|
||||
message: `Interested in sponsoring ${entityName} as ${slotTier} sponsor.`,
|
||||
};
|
||||
|
||||
// This would be: await sponsorshipService.submitSponsorshipRequest(request);
|
||||
// For now, we'll log it as a placeholder
|
||||
console.log('Sponsorship request:', request);
|
||||
|
||||
// Mark as applied
|
||||
setAppliedTiers(prev => new Set([...prev, slotTier]));
|
||||
|
||||
// Call callback if provided
|
||||
onSponsorshipRequested?.(slotTier);
|
||||
|
||||
} catch (err) {
|
||||
@@ -155,214 +122,174 @@ export default function SponsorInsightsCard({
|
||||
} finally {
|
||||
setApplyingTier(null);
|
||||
}
|
||||
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, router, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
|
||||
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, onNavigate, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
|
||||
|
||||
return (
|
||||
<Card className={`mb-6 border-primary-blue/30 bg-gradient-to-r ${tierStyles.gradient}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Target className="w-5 h-5 text-primary-blue" />
|
||||
<h3 className="text-lg font-semibold text-white">Sponsorship Opportunity</h3>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm">
|
||||
<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)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`px-2 py-1 rounded text-xs font-medium border ${tierStyles.badge}`}>
|
||||
{tier.charAt(0).toUpperCase() + tier.slice(1)} {getEntityLabel(entityType)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
<SponsorshipTierBadge tier={tier} entityLabel={getEntityLabel(entityType)} />
|
||||
</Box>
|
||||
|
||||
{/* Key Metrics Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||
{metrics.slice(0, 4).map((metric, index) => {
|
||||
const Icon = metric.icon as React.ComponentType<{ className?: string }>;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-iron-gray/50 rounded-lg p-3 border border-charcoal-outline"
|
||||
>
|
||||
<div className={`flex items-center gap-1.5 ${metric.color || 'text-primary-blue'} mb-1`}>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">{metric.label}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-xl font-bold text-white">
|
||||
{typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value}
|
||||
</div>
|
||||
{metric.trend && (
|
||||
<span className={`text-xs ${metric.trend.isPositive ? 'text-performance-green' : 'text-red-400'}`}>
|
||||
{metric.trend.isPositive ? '+' : ''}{metric.trend.value}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={3} mb={4}>
|
||||
{metrics.slice(0, 4).map((metric, index) => (
|
||||
<SponsorMetricCard
|
||||
key={index}
|
||||
label={metric.label}
|
||||
value={metric.value}
|
||||
icon={metric.icon as LucideIcon}
|
||||
color={metric.color}
|
||||
trend={metric.trend}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Trust & Activity Indicators */}
|
||||
{(trustScore !== undefined || discordMembers !== undefined || monthlyActivity !== undefined) && (
|
||||
<div className="flex flex-wrap gap-4 mb-4 pb-4 border-b border-charcoal-outline/50">
|
||||
<Box display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
|
||||
{trustScore !== undefined && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-performance-green" />
|
||||
<span className="text-sm text-gray-400">Trust Score:</span>
|
||||
<span className="text-sm font-semibold text-white">{trustScore}/100</span>
|
||||
</div>
|
||||
<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 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-sm text-gray-400">Discord:</span>
|
||||
<span className="text-sm font-semibold text-white">{discordMembers.toLocaleString()}</span>
|
||||
</div>
|
||||
<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 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-neon-aqua" />
|
||||
<span className="text-sm text-gray-400">Monthly Activity:</span>
|
||||
<span className="text-sm font-semibold text-white">{monthlyActivity}%</span>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Sponsorship Slots */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||||
{/* Main Sponsor Slot */}
|
||||
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={3} mb={4}>
|
||||
{mainSlot && (
|
||||
<div className={`p-3 rounded-lg border ${
|
||||
mainSlot.available
|
||||
? 'bg-performance-green/10 border-performance-green/30'
|
||||
: 'bg-iron-gray/30 border-charcoal-outline'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-white">Main Sponsor Slot</span>
|
||||
<span className={`text-xs ${mainSlot.available ? 'text-performance-green' : 'text-gray-500'}`}>
|
||||
{mainSlot.available ? 'Available' : 'Taken'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-2">{mainSlot.benefits.join(' • ')}</p>
|
||||
{mainSlot.available && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-bold text-white">
|
||||
${mainSlot.price.toLocaleString()}/season
|
||||
</span>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSponsorClick('main')}
|
||||
disabled={applyingTier === 'main'}
|
||||
className="text-xs px-3 py-1"
|
||||
>
|
||||
{applyingTier === 'main' ? (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
Applying...
|
||||
</>
|
||||
) : appliedTiers.has('main') ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
Applied
|
||||
</>
|
||||
) : (
|
||||
'Apply to Sponsor'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Secondary Slots */}
|
||||
{secondarySlots.length > 0 && (
|
||||
<div className={`p-3 rounded-lg border ${
|
||||
availableSecondary > 0
|
||||
? 'bg-purple-500/10 border-purple-500/30'
|
||||
: 'bg-iron-gray/30 border-charcoal-outline'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-white">Secondary Slots</span>
|
||||
<span className={`text-xs ${availableSecondary > 0 ? 'text-purple-400' : 'text-gray-500'}`}>
|
||||
{availableSecondary}/{secondarySlots.length} Available
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-2">
|
||||
{secondarySlots[0]?.benefits.join(' • ') || 'Logo placement on page'}
|
||||
</p>
|
||||
{availableSecondary > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-bold text-white">
|
||||
${secondarySlots[0]?.price.toLocaleString()}/season
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSponsorClick('secondary')}
|
||||
disabled={applyingTier === 'secondary'}
|
||||
className="text-xs px-3 py-1"
|
||||
>
|
||||
{applyingTier === 'secondary' ? (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
Applying...
|
||||
</>
|
||||
) : appliedTiers.has('secondary') ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
Applied
|
||||
</>
|
||||
) : (
|
||||
'Apply to Sponsor'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* Additional Stats */}
|
||||
{additionalStats && (
|
||||
<div className="mb-4 pb-4 border-b border-charcoal-outline/50">
|
||||
<h4 className="text-sm font-medium text-gray-400 mb-2">{additionalStats.label}</h4>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<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) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">{item.label}:</span>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<p className="text-sm text-red-400">{error}</p>
|
||||
</div>
|
||||
<InfoBox
|
||||
variant="warning"
|
||||
icon={Target}
|
||||
title="Error"
|
||||
description={error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-charcoal-outline/50">
|
||||
<p className="text-xs text-gray-500">
|
||||
10% platform fee applies • Logos burned on all liveries • Sponsorships are attached to seasons, so you can change partners from season to season
|
||||
<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'}
|
||||
</p>
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => router.push(ctaHref || `/sponsor/${entityType}s/${entityId}`)}
|
||||
className="text-xs"
|
||||
onClick={() => onNavigate(ctaHref || `/sponsor/${entityType}s/${entityId}`)}
|
||||
size="sm"
|
||||
>
|
||||
{ctaLabel || 'View Full Details'}
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user