595 lines
19 KiB
TypeScript
595 lines
19 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import { useServices } from '@/lib/services/ServiceProvider';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
import {
|
|
Eye,
|
|
TrendingUp,
|
|
Users,
|
|
Star,
|
|
Target,
|
|
DollarSign,
|
|
Calendar,
|
|
Trophy,
|
|
Zap,
|
|
ExternalLink,
|
|
MessageCircle,
|
|
Activity,
|
|
Shield,
|
|
Check,
|
|
Loader2,
|
|
} from 'lucide-react';
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
export type EntityType = 'league' | 'race' | 'driver' | 'team';
|
|
|
|
export interface SponsorMetric {
|
|
icon: React.ElementType;
|
|
label: string;
|
|
value: string | number;
|
|
color?: string;
|
|
trend?: {
|
|
value: number;
|
|
isPositive: boolean;
|
|
};
|
|
}
|
|
|
|
export interface SponsorshipSlot {
|
|
tier: 'main' | 'secondary';
|
|
available: boolean;
|
|
price: number;
|
|
currency?: string;
|
|
benefits: string[];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
function getTierStyles(tier: SponsorInsightsProps['tier']) {
|
|
switch (tier) {
|
|
case 'premium':
|
|
return {
|
|
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
|
|
gradient: 'from-yellow-500/10 via-transparent to-transparent',
|
|
};
|
|
case 'standard':
|
|
return {
|
|
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
|
gradient: 'from-blue-500/10 via-transparent to-transparent',
|
|
};
|
|
default:
|
|
return {
|
|
badge: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
|
|
gradient: 'from-gray-500/10 via-transparent to-transparent',
|
|
};
|
|
}
|
|
}
|
|
|
|
function getEntityLabel(type: EntityType): string {
|
|
switch (type) {
|
|
case 'league': return 'League';
|
|
case 'race': return 'Race';
|
|
case 'driver': return 'Driver';
|
|
case 'team': return 'Team';
|
|
}
|
|
}
|
|
|
|
function getEntityIcon(type: EntityType) {
|
|
switch (type) {
|
|
case 'league': return Trophy;
|
|
case 'race': return Zap;
|
|
case 'driver': return Users;
|
|
case 'team': return Users;
|
|
}
|
|
}
|
|
|
|
function getSponsorshipTagline(type: EntityType): string {
|
|
if (type === 'league') {
|
|
return 'Reach engaged sim racers by sponsoring a season in this league.';
|
|
}
|
|
return `Reach engaged sim racers by sponsoring this ${getEntityLabel(type).toLowerCase()}`;
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMPONENT
|
|
// ============================================================================
|
|
|
|
export default function SponsorInsightsCard({
|
|
entityType,
|
|
entityId,
|
|
entityName,
|
|
tier,
|
|
metrics,
|
|
slots,
|
|
additionalStats,
|
|
trustScore,
|
|
discordMembers,
|
|
monthlyActivity,
|
|
ctaLabel,
|
|
ctaHref,
|
|
currentSponsorId,
|
|
onSponsorshipRequested,
|
|
}: SponsorInsightsProps) {
|
|
const router = useRouter();
|
|
const { sponsorshipService } = useServices();
|
|
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);
|
|
|
|
const mainSlot = slots.find(s => s.tier === 'main');
|
|
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 'race': return 'race';
|
|
case 'driver': return 'driver';
|
|
case 'team': return 'team';
|
|
}
|
|
}, []);
|
|
|
|
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);
|
|
return;
|
|
}
|
|
|
|
// If already applied for this tier, show details page
|
|
if (appliedTiers.has(slotTier)) {
|
|
router.push(`/sponsor/dashboard`);
|
|
return;
|
|
}
|
|
|
|
// Apply for sponsorship using service
|
|
setApplyingTier(slotTier);
|
|
setError(null);
|
|
|
|
try {
|
|
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
|
|
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) {
|
|
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, router, 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">
|
|
{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>
|
|
|
|
{/* 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;
|
|
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>
|
|
|
|
{/* 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">
|
|
{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>
|
|
)}
|
|
{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>
|
|
)}
|
|
{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>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Sponsorship Slots */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
|
{/* Main Sponsor Slot */}
|
|
{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>
|
|
)}
|
|
|
|
{/* 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>
|
|
)}
|
|
</div>
|
|
|
|
{/* 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">
|
|
{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">
|
|
{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 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>
|
|
)}
|
|
|
|
{/* 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
|
|
{appliedTiers.size > 0 && ' • Application pending review'}
|
|
</p>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => router.push(ctaHref || `/sponsor/${entityType}s/${entityId}`)}
|
|
className="text-xs"
|
|
>
|
|
{ctaLabel || 'View Full Details'}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// HELPER HOOK: useSponsorMode
|
|
// ============================================================================
|
|
|
|
export function useSponsorMode(): boolean {
|
|
const { session } = useAuth();
|
|
const [isSponsor, setIsSponsor] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (!session?.user) {
|
|
setIsSponsor(false);
|
|
return;
|
|
}
|
|
|
|
// Check session.user.role for sponsor
|
|
const role = (session.user as any).role;
|
|
if (role === 'sponsor') {
|
|
setIsSponsor(true);
|
|
return;
|
|
}
|
|
|
|
// Fallback: check email patterns
|
|
const email = session.user.email?.toLowerCase() || '';
|
|
const displayName = session.user.displayName?.toLowerCase() || '';
|
|
|
|
setIsSponsor(email.includes('sponsor') || displayName.includes('sponsor'));
|
|
}, [session]);
|
|
|
|
return isSponsor;
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMMON METRIC BUILDERS
|
|
// ============================================================================
|
|
|
|
export const MetricBuilders = {
|
|
views: (value: number, label = 'Views'): SponsorMetric => ({
|
|
icon: Eye,
|
|
label,
|
|
value,
|
|
color: 'text-primary-blue',
|
|
}),
|
|
|
|
engagement: (value: number | string): SponsorMetric => ({
|
|
icon: TrendingUp,
|
|
label: 'Engagement',
|
|
value: typeof value === 'number' ? `${value}%` : value,
|
|
color: 'text-performance-green',
|
|
}),
|
|
|
|
reach: (value: number): SponsorMetric => ({
|
|
icon: Users,
|
|
label: 'Est. Reach',
|
|
value,
|
|
color: 'text-purple-400',
|
|
}),
|
|
|
|
rating: (value: number | string, label = 'Rating'): SponsorMetric => ({
|
|
icon: Star,
|
|
label,
|
|
value,
|
|
color: 'text-warning-amber',
|
|
}),
|
|
|
|
races: (value: number): SponsorMetric => ({
|
|
icon: Calendar,
|
|
label: 'Races',
|
|
value,
|
|
color: 'text-neon-aqua',
|
|
}),
|
|
|
|
members: (value: number): SponsorMetric => ({
|
|
icon: Users,
|
|
label: 'Members',
|
|
value,
|
|
color: 'text-purple-400',
|
|
}),
|
|
|
|
impressions: (value: number): SponsorMetric => ({
|
|
icon: Eye,
|
|
label: 'Impressions',
|
|
value,
|
|
color: 'text-primary-blue',
|
|
}),
|
|
|
|
sof: (value: number | string): SponsorMetric => ({
|
|
icon: Zap,
|
|
label: 'Avg SOF',
|
|
value,
|
|
color: 'text-warning-amber',
|
|
}),
|
|
};
|
|
|
|
// ============================================================================
|
|
// SLOT TEMPLATES
|
|
// ============================================================================
|
|
|
|
export const SlotTemplates = {
|
|
league: (mainAvailable: boolean, secondaryAvailable: number, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
|
|
{
|
|
tier: 'main',
|
|
available: mainAvailable,
|
|
price: mainPrice,
|
|
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
|
|
},
|
|
{
|
|
tier: 'secondary',
|
|
available: secondaryAvailable > 0,
|
|
price: secondaryPrice,
|
|
benefits: ['Side logo placement', 'League page listing'],
|
|
},
|
|
{
|
|
tier: 'secondary',
|
|
available: secondaryAvailable > 1,
|
|
price: secondaryPrice,
|
|
benefits: ['Side logo placement', 'League page listing'],
|
|
},
|
|
],
|
|
|
|
race: (mainAvailable: boolean, mainPrice: number): SponsorshipSlot[] => [
|
|
{
|
|
tier: 'main',
|
|
available: mainAvailable,
|
|
price: mainPrice,
|
|
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
|
|
},
|
|
],
|
|
|
|
driver: (available: boolean, price: number): SponsorshipSlot[] => [
|
|
{
|
|
tier: 'main',
|
|
available,
|
|
price,
|
|
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
|
|
},
|
|
],
|
|
|
|
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
|
|
{
|
|
tier: 'main',
|
|
available: mainAvailable,
|
|
price: mainPrice,
|
|
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
|
|
},
|
|
{
|
|
tier: 'secondary',
|
|
available: secondaryAvailable,
|
|
price: secondaryPrice,
|
|
benefits: ['Team page logo', 'Minor livery placement'],
|
|
},
|
|
],
|
|
}; |