866 lines
31 KiB
TypeScript
866 lines
31 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { motion, useReducedMotion, AnimatePresence } from 'framer-motion';
|
|
import Link from 'next/link';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import StatusBadge from '@/components/ui/StatusBadge';
|
|
import InfoBanner from '@/components/ui/InfoBanner';
|
|
import {
|
|
Megaphone,
|
|
Trophy,
|
|
Users,
|
|
Eye,
|
|
Calendar,
|
|
ExternalLink,
|
|
Plus,
|
|
ChevronRight,
|
|
Check,
|
|
Clock,
|
|
XCircle,
|
|
Car,
|
|
Flag,
|
|
Search,
|
|
Filter,
|
|
TrendingUp,
|
|
BarChart3,
|
|
ArrowUpRight,
|
|
ArrowDownRight,
|
|
AlertCircle,
|
|
Send,
|
|
ThumbsUp,
|
|
ThumbsDown,
|
|
RefreshCw,
|
|
Handshake
|
|
} from 'lucide-react';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
type SponsorshipType = 'all' | 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
|
|
type SponsorshipStatus = 'all' | 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
|
|
|
|
interface Sponsorship {
|
|
id: string;
|
|
type: SponsorshipType;
|
|
entityId: string;
|
|
entityName: string;
|
|
tier?: 'main' | 'secondary';
|
|
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
|
|
applicationDate?: Date;
|
|
approvalDate?: Date;
|
|
rejectionReason?: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
price: number;
|
|
impressions: number;
|
|
impressionsChange?: number;
|
|
engagement?: number;
|
|
details?: string;
|
|
// For pending approvals
|
|
entityOwner?: string;
|
|
applicationMessage?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Mock Data - Updated to show application workflow
|
|
// ============================================================================
|
|
|
|
const MOCK_SPONSORSHIPS: Sponsorship[] = [
|
|
// Active sponsorships (approved and started)
|
|
{
|
|
id: 's1',
|
|
type: 'leagues',
|
|
entityId: 'l1',
|
|
entityName: 'GT3 Masters Championship',
|
|
tier: 'main',
|
|
status: 'active',
|
|
applicationDate: new Date('2025-09-15'),
|
|
approvalDate: new Date('2025-09-18'),
|
|
startDate: new Date('2025-10-01'),
|
|
endDate: new Date('2026-02-28'),
|
|
price: 1200,
|
|
impressions: 45200,
|
|
impressionsChange: 12.5,
|
|
engagement: 4.2,
|
|
details: '48 drivers • 12 races',
|
|
entityOwner: 'Championship Admin',
|
|
},
|
|
{
|
|
id: 's2',
|
|
type: 'leagues',
|
|
entityId: 'l2',
|
|
entityName: 'Endurance Pro Series',
|
|
tier: 'secondary',
|
|
status: 'active',
|
|
applicationDate: new Date('2025-10-20'),
|
|
approvalDate: new Date('2025-10-25'),
|
|
startDate: new Date('2025-11-01'),
|
|
endDate: new Date('2026-03-31'),
|
|
price: 500,
|
|
impressions: 38400,
|
|
impressionsChange: 8.3,
|
|
engagement: 3.8,
|
|
details: '72 drivers • 6 races',
|
|
entityOwner: 'Endurance Racing LLC',
|
|
},
|
|
{
|
|
id: 's3',
|
|
type: 'teams',
|
|
entityId: 't1',
|
|
entityName: 'Velocity Racing',
|
|
status: 'active',
|
|
applicationDate: new Date('2025-08-25'),
|
|
approvalDate: new Date('2025-08-26'),
|
|
startDate: new Date('2025-09-01'),
|
|
endDate: new Date('2026-08-31'),
|
|
price: 400,
|
|
impressions: 12300,
|
|
impressionsChange: 5.2,
|
|
engagement: 5.1,
|
|
details: '4 drivers • GT3 & LMP',
|
|
entityOwner: 'Team Principal',
|
|
},
|
|
// Pending approval (waiting for entity owner)
|
|
{
|
|
id: 's9',
|
|
type: 'leagues',
|
|
entityId: 'l3',
|
|
entityName: 'Formula Sim Series',
|
|
tier: 'main',
|
|
status: 'pending_approval',
|
|
applicationDate: new Date('2025-12-14'),
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: new Date('2026-06-30'),
|
|
price: 1500,
|
|
impressions: 0,
|
|
details: '36 drivers • F3 class',
|
|
entityOwner: 'Formula Sim Organization',
|
|
applicationMessage: 'We would love to be your main sponsor for the upcoming season.',
|
|
},
|
|
{
|
|
id: 's10',
|
|
type: 'teams',
|
|
entityId: 't3',
|
|
entityName: 'Phoenix Racing Team',
|
|
status: 'pending_approval',
|
|
applicationDate: new Date('2025-12-12'),
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: new Date('2026-12-31'),
|
|
price: 600,
|
|
impressions: 0,
|
|
details: '5 drivers • Multi-class',
|
|
entityOwner: 'Phoenix Team Manager',
|
|
applicationMessage: 'Interested in sponsoring your team for the full 2026 season.',
|
|
},
|
|
{
|
|
id: 's11',
|
|
type: 'drivers',
|
|
entityId: 'd3',
|
|
entityName: 'James Rodriguez',
|
|
status: 'pending_approval',
|
|
applicationDate: new Date('2025-12-10'),
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: new Date('2026-12-31'),
|
|
price: 250,
|
|
impressions: 0,
|
|
details: 'Rising rookie • GT3 Masters',
|
|
entityOwner: 'James Rodriguez',
|
|
applicationMessage: 'Would like to support your racing career.',
|
|
},
|
|
// Recently approved (not yet started)
|
|
{
|
|
id: 's12',
|
|
type: 'races',
|
|
entityId: 'r1',
|
|
entityName: 'Spa 24 Hours',
|
|
status: 'approved',
|
|
applicationDate: new Date('2025-12-01'),
|
|
approvalDate: new Date('2025-12-05'),
|
|
startDate: new Date('2025-12-20'),
|
|
endDate: new Date('2025-12-21'),
|
|
price: 300,
|
|
impressions: 0,
|
|
details: 'Endurance Pro Series • Dec 20-21',
|
|
entityOwner: 'Race Director',
|
|
},
|
|
{
|
|
id: 's13',
|
|
type: 'drivers',
|
|
entityId: 'd4',
|
|
entityName: 'Emma Wilson',
|
|
status: 'approved',
|
|
applicationDate: new Date('2025-12-08'),
|
|
approvalDate: new Date('2025-12-10'),
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: new Date('2026-12-31'),
|
|
price: 180,
|
|
impressions: 0,
|
|
details: 'Touring Car specialist',
|
|
entityOwner: 'Emma Wilson',
|
|
},
|
|
// Rejected applications
|
|
{
|
|
id: 's14',
|
|
type: 'leagues',
|
|
entityId: 'l4',
|
|
entityName: 'Elite GT Championship',
|
|
tier: 'main',
|
|
status: 'rejected',
|
|
applicationDate: new Date('2025-11-20'),
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: new Date('2026-06-30'),
|
|
price: 2000,
|
|
impressions: 0,
|
|
details: '24 drivers • Invite-only',
|
|
entityOwner: 'Elite Racing Committee',
|
|
rejectionReason: 'Main sponsor position already filled for the upcoming season.',
|
|
},
|
|
{
|
|
id: 's15',
|
|
type: 'teams',
|
|
entityId: 't4',
|
|
entityName: 'Apex Motorsport',
|
|
status: 'rejected',
|
|
applicationDate: new Date('2025-11-15'),
|
|
startDate: new Date('2026-01-01'),
|
|
endDate: new Date('2026-12-31'),
|
|
price: 450,
|
|
impressions: 0,
|
|
details: '3 drivers • LMP2',
|
|
entityOwner: 'Apex Team Owner',
|
|
rejectionReason: 'Already have exclusive sponsor agreement in this category.',
|
|
},
|
|
// Existing active ones
|
|
{
|
|
id: 's4',
|
|
type: 'teams',
|
|
entityId: 't2',
|
|
entityName: 'Storm Motorsport',
|
|
status: 'active',
|
|
applicationDate: new Date('2025-10-01'),
|
|
approvalDate: new Date('2025-10-05'),
|
|
startDate: new Date('2025-10-15'),
|
|
endDate: new Date('2026-10-14'),
|
|
price: 350,
|
|
impressions: 8900,
|
|
impressionsChange: -2.1,
|
|
engagement: 4.5,
|
|
details: '3 drivers • Formula',
|
|
entityOwner: 'Storm Racing LLC',
|
|
},
|
|
{
|
|
id: 's5',
|
|
type: 'drivers',
|
|
entityId: 'd1',
|
|
entityName: 'Max Velocity',
|
|
status: 'active',
|
|
applicationDate: new Date('2025-10-20'),
|
|
approvalDate: new Date('2025-10-21'),
|
|
startDate: new Date('2025-11-01'),
|
|
endDate: new Date('2026-10-31'),
|
|
price: 200,
|
|
impressions: 8200,
|
|
impressionsChange: 15.8,
|
|
engagement: 6.2,
|
|
details: 'Velocity Racing • P1 in GT3 Masters',
|
|
entityOwner: 'Max Velocity',
|
|
},
|
|
{
|
|
id: 's6',
|
|
type: 'drivers',
|
|
entityId: 'd2',
|
|
entityName: 'Sarah Storm',
|
|
status: 'active',
|
|
applicationDate: new Date('2025-09-25'),
|
|
approvalDate: new Date('2025-09-26'),
|
|
startDate: new Date('2025-10-01'),
|
|
endDate: new Date('2026-09-30'),
|
|
price: 150,
|
|
impressions: 6100,
|
|
impressionsChange: 22.4,
|
|
engagement: 5.8,
|
|
details: 'Storm Motorsport • Rising star',
|
|
entityOwner: 'Sarah Storm',
|
|
},
|
|
{
|
|
id: 's8',
|
|
type: 'platform',
|
|
entityId: 'p1',
|
|
entityName: 'Homepage Banner',
|
|
status: 'active',
|
|
applicationDate: new Date('2025-11-25'),
|
|
approvalDate: new Date('2025-11-25'),
|
|
startDate: new Date('2025-12-01'),
|
|
endDate: new Date('2025-12-31'),
|
|
price: 500,
|
|
impressions: 52000,
|
|
impressionsChange: 3.4,
|
|
engagement: 2.1,
|
|
details: 'Header position • All pages',
|
|
entityOwner: 'GridPilot',
|
|
},
|
|
];
|
|
|
|
// ============================================================================
|
|
// Configuration
|
|
// ============================================================================
|
|
|
|
const TYPE_CONFIG = {
|
|
leagues: { icon: Trophy, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', label: 'League' },
|
|
teams: { icon: Users, color: 'text-purple-400', bgColor: 'bg-purple-400/10', label: 'Team' },
|
|
drivers: { icon: Car, color: 'text-performance-green', bgColor: 'bg-performance-green/10', label: 'Driver' },
|
|
races: { icon: Flag, color: 'text-warning-amber', bgColor: 'bg-warning-amber/10', label: 'Race' },
|
|
platform: { icon: Megaphone, color: 'text-racing-red', bgColor: 'bg-racing-red/10', label: 'Platform' },
|
|
all: { icon: BarChart3, color: 'text-gray-400', bgColor: 'bg-gray-400/10', label: 'All' },
|
|
};
|
|
|
|
const STATUS_CONFIG = {
|
|
active: {
|
|
icon: Check,
|
|
color: 'text-performance-green',
|
|
bgColor: 'bg-performance-green/10',
|
|
borderColor: 'border-performance-green/30',
|
|
label: 'Active'
|
|
},
|
|
pending_approval: {
|
|
icon: Clock,
|
|
color: 'text-warning-amber',
|
|
bgColor: 'bg-warning-amber/10',
|
|
borderColor: 'border-warning-amber/30',
|
|
label: 'Awaiting Approval'
|
|
},
|
|
approved: {
|
|
icon: ThumbsUp,
|
|
color: 'text-primary-blue',
|
|
bgColor: 'bg-primary-blue/10',
|
|
borderColor: 'border-primary-blue/30',
|
|
label: 'Approved'
|
|
},
|
|
rejected: {
|
|
icon: ThumbsDown,
|
|
color: 'text-racing-red',
|
|
bgColor: 'bg-racing-red/10',
|
|
borderColor: 'border-racing-red/30',
|
|
label: 'Declined'
|
|
},
|
|
expired: {
|
|
icon: XCircle,
|
|
color: 'text-gray-400',
|
|
bgColor: 'bg-gray-400/10',
|
|
borderColor: 'border-gray-400/30',
|
|
label: 'Expired'
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Components
|
|
// ============================================================================
|
|
|
|
function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
|
const router = useRouter();
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
const typeConfig = TYPE_CONFIG[sponsorship.type as keyof typeof TYPE_CONFIG];
|
|
const statusConfig = STATUS_CONFIG[sponsorship.status];
|
|
const TypeIcon = typeConfig.icon;
|
|
const StatusIcon = statusConfig.icon;
|
|
|
|
const daysRemaining = Math.ceil((sponsorship.endDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
const isExpiringSoon = daysRemaining > 0 && daysRemaining <= 30;
|
|
const isPending = sponsorship.status === 'pending_approval';
|
|
const isRejected = sponsorship.status === 'rejected';
|
|
const isApproved = sponsorship.status === 'approved';
|
|
|
|
const getEntityLink = () => {
|
|
switch (sponsorship.type) {
|
|
case 'leagues': return `/leagues/${sponsorship.entityId}`;
|
|
case 'teams': return `/teams/${sponsorship.entityId}`;
|
|
case 'drivers': return `/drivers/${sponsorship.entityId}`;
|
|
case 'races': return `/races/${sponsorship.entityId}`;
|
|
default: return '#';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
whileHover={{ y: -2 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<Card className={`hover:border-primary-blue/30 transition-all duration-300 ${
|
|
isPending ? 'border-warning-amber/30' :
|
|
isRejected ? 'border-racing-red/20 opacity-75' :
|
|
isApproved ? 'border-primary-blue/30' : ''
|
|
}`}>
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-10 h-10 rounded-lg ${typeConfig.bgColor} flex items-center justify-center`}>
|
|
<TypeIcon className={`w-5 h-5 ${typeConfig.color}`} />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className={`text-xs font-medium px-2 py-0.5 rounded ${typeConfig.bgColor} ${typeConfig.color}`}>
|
|
{typeConfig.label}
|
|
</span>
|
|
{sponsorship.tier && (
|
|
<span className={`text-xs font-medium px-2 py-0.5 rounded ${
|
|
sponsorship.tier === 'main'
|
|
? 'bg-primary-blue/20 text-primary-blue'
|
|
: 'bg-purple-400/20 text-purple-400'
|
|
}`}>
|
|
{sponsorship.tier === 'main' ? 'Main Sponsor' : 'Secondary'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className={`flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium border ${statusConfig.bgColor} ${statusConfig.color} ${statusConfig.borderColor}`}>
|
|
<StatusIcon className="w-3 h-3" />
|
|
{statusConfig.label}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Entity Name */}
|
|
<h3 className="text-lg font-semibold text-white mb-1">{sponsorship.entityName}</h3>
|
|
{sponsorship.details && (
|
|
<p className="text-sm text-gray-500 mb-3">{sponsorship.details}</p>
|
|
)}
|
|
|
|
{/* Application/Approval Info for non-active states */}
|
|
{isPending && (
|
|
<div className="mb-4 p-3 rounded-lg bg-warning-amber/5 border border-warning-amber/20">
|
|
<div className="flex items-center gap-2 text-warning-amber text-sm mb-2">
|
|
<Send className="w-4 h-4" />
|
|
<span className="font-medium">Application Pending</span>
|
|
</div>
|
|
<p className="text-xs text-gray-400 mb-2">
|
|
Sent to <span className="text-gray-300">{sponsorship.entityOwner}</span> on{' '}
|
|
{sponsorship.applicationDate?.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
|
</p>
|
|
{sponsorship.applicationMessage && (
|
|
<p className="text-xs text-gray-500 italic">"{sponsorship.applicationMessage}"</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{isApproved && (
|
|
<div className="mb-4 p-3 rounded-lg bg-primary-blue/5 border border-primary-blue/20">
|
|
<div className="flex items-center gap-2 text-primary-blue text-sm mb-1">
|
|
<ThumbsUp className="w-4 h-4" />
|
|
<span className="font-medium">Approved!</span>
|
|
</div>
|
|
<p className="text-xs text-gray-400">
|
|
Approved by <span className="text-gray-300">{sponsorship.entityOwner}</span> on{' '}
|
|
{sponsorship.approvalDate?.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Starts {sponsorship.startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{isRejected && (
|
|
<div className="mb-4 p-3 rounded-lg bg-racing-red/5 border border-racing-red/20">
|
|
<div className="flex items-center gap-2 text-racing-red text-sm mb-1">
|
|
<ThumbsDown className="w-4 h-4" />
|
|
<span className="font-medium">Application Declined</span>
|
|
</div>
|
|
{sponsorship.rejectionReason && (
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
Reason: <span className="text-gray-300">{sponsorship.rejectionReason}</span>
|
|
</p>
|
|
)}
|
|
<Button variant="secondary" className="mt-2 text-xs">
|
|
<RefreshCw className="w-3 h-3 mr-1" />
|
|
Reapply
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Metrics Grid - Only show for active sponsorships */}
|
|
{sponsorship.status === 'active' && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
|
<div className="bg-iron-gray/50 rounded-lg p-3">
|
|
<div className="flex items-center gap-1 text-gray-400 text-xs mb-1">
|
|
<Eye className="w-3 h-3" />
|
|
Impressions
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-white font-semibold">{sponsorship.impressions.toLocaleString()}</span>
|
|
{sponsorship.impressionsChange !== undefined && sponsorship.impressionsChange !== 0 && (
|
|
<span className={`text-xs flex items-center ${
|
|
sponsorship.impressionsChange > 0 ? 'text-performance-green' : 'text-racing-red'
|
|
}`}>
|
|
{sponsorship.impressionsChange > 0 ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
|
|
{Math.abs(sponsorship.impressionsChange)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{sponsorship.engagement && (
|
|
<div className="bg-iron-gray/50 rounded-lg p-3">
|
|
<div className="flex items-center gap-1 text-gray-400 text-xs mb-1">
|
|
<TrendingUp className="w-3 h-3" />
|
|
Engagement
|
|
</div>
|
|
<div className="text-white font-semibold">{sponsorship.engagement}%</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-iron-gray/50 rounded-lg p-3">
|
|
<div className="flex items-center gap-1 text-gray-400 text-xs mb-1">
|
|
<Calendar className="w-3 h-3" />
|
|
Period
|
|
</div>
|
|
<div className="text-white font-semibold text-xs">
|
|
{sponsorship.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - {sponsorship.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-iron-gray/50 rounded-lg p-3">
|
|
<div className="flex items-center gap-1 text-gray-400 text-xs mb-1">
|
|
<Trophy className="w-3 h-3" />
|
|
Investment
|
|
</div>
|
|
<div className="text-white font-semibold">${sponsorship.price}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Basic info for non-active */}
|
|
{sponsorship.status !== 'active' && (
|
|
<div className="flex items-center gap-4 mb-4 text-sm">
|
|
<div className="flex items-center gap-1 text-gray-400">
|
|
<Calendar className="w-3.5 h-3.5" />
|
|
{sponsorship.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - {sponsorship.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
|
</div>
|
|
<div className="flex items-center gap-1 text-gray-400">
|
|
<Trophy className="w-3.5 h-3.5" />
|
|
${sponsorship.price}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-between pt-3 border-t border-charcoal-outline/50">
|
|
<div className="flex items-center gap-2">
|
|
{sponsorship.status === 'active' && (
|
|
<span className={`text-xs ${isExpiringSoon ? 'text-warning-amber' : 'text-gray-500'}`}>
|
|
{daysRemaining > 0 ? `${daysRemaining} days remaining` : 'Ended'}
|
|
</span>
|
|
)}
|
|
{isPending && (
|
|
<span className="text-xs text-gray-500">
|
|
Waiting for response...
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{sponsorship.type !== 'platform' && (
|
|
<Link href={getEntityLink()}>
|
|
<Button variant="secondary" className="text-xs">
|
|
<ExternalLink className="w-3 h-3 mr-1" />
|
|
View
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
{isPending && (
|
|
<Button variant="secondary" className="text-xs text-racing-red hover:bg-racing-red/10">
|
|
Cancel Application
|
|
</Button>
|
|
)}
|
|
{sponsorship.status === 'active' && (
|
|
<Button variant="secondary" className="text-xs">
|
|
Details
|
|
<ChevronRight className="w-3 h-3 ml-1" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Component
|
|
// ============================================================================
|
|
|
|
export default function SponsorCampaignsPage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
const initialType = (searchParams.get('type') as SponsorshipType) || 'all';
|
|
const [typeFilter, setTypeFilter] = useState<SponsorshipType>(initialType);
|
|
const [statusFilter, setStatusFilter] = useState<SponsorshipStatus>('all');
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setLoading(false), 300);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
// Filter sponsorships
|
|
const filteredSponsorships = MOCK_SPONSORSHIPS.filter(s => {
|
|
if (typeFilter !== 'all' && s.type !== typeFilter) return false;
|
|
if (statusFilter !== 'all' && s.status !== statusFilter) return false;
|
|
if (searchQuery && !s.entityName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
|
return true;
|
|
});
|
|
|
|
// Calculate stats
|
|
const stats = {
|
|
total: MOCK_SPONSORSHIPS.length,
|
|
active: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').length,
|
|
pending: MOCK_SPONSORSHIPS.filter(s => s.status === 'pending_approval').length,
|
|
approved: MOCK_SPONSORSHIPS.filter(s => s.status === 'approved').length,
|
|
rejected: MOCK_SPONSORSHIPS.filter(s => s.status === 'rejected').length,
|
|
totalInvestment: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').reduce((sum, s) => sum + s.price, 0),
|
|
totalImpressions: MOCK_SPONSORSHIPS.reduce((sum, s) => sum + s.impressions, 0),
|
|
};
|
|
|
|
// Stats by type
|
|
const statsByType = {
|
|
leagues: MOCK_SPONSORSHIPS.filter(s => s.type === 'leagues').length,
|
|
teams: MOCK_SPONSORSHIPS.filter(s => s.type === 'teams').length,
|
|
drivers: MOCK_SPONSORSHIPS.filter(s => s.type === 'drivers').length,
|
|
races: MOCK_SPONSORSHIPS.filter(s => s.type === 'races').length,
|
|
platform: MOCK_SPONSORSHIPS.filter(s => s.type === 'platform').length,
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="max-w-7xl mx-auto py-8 px-4 flex items-center justify-center min-h-[400px]">
|
|
<div className="text-center">
|
|
<div className="w-8 h-8 border-2 border-primary-blue border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
<p className="text-gray-400">Loading sponsorships...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto py-8 px-4">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
|
|
<Megaphone className="w-7 h-7 text-primary-blue" />
|
|
My Sponsorships
|
|
</h1>
|
|
<p className="text-gray-400 mt-1">Manage applications and active sponsorship campaigns</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Link href="/leagues">
|
|
<Button variant="primary">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Find Opportunities
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info Banner about how sponsorships work */}
|
|
{stats.pending > 0 && (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="mb-6"
|
|
>
|
|
<InfoBanner type="info" title="Sponsorship Applications">
|
|
<p>
|
|
You have <strong className="text-white">{stats.pending} pending application{stats.pending !== 1 ? 's' : ''}</strong> waiting for approval.
|
|
League admins, team owners, and drivers review applications before accepting sponsorships.
|
|
</p>
|
|
</InfoBanner>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Quick Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-8">
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
>
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-white">{stats.total}</div>
|
|
<div className="text-sm text-gray-400">Total</div>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.05 }}
|
|
>
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-performance-green">{stats.active}</div>
|
|
<div className="text-sm text-gray-400">Active</div>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.1 }}
|
|
>
|
|
<Card className={`p-4 ${stats.pending > 0 ? 'border-warning-amber/30' : ''}`}>
|
|
<div className="text-2xl font-bold text-warning-amber">{stats.pending}</div>
|
|
<div className="text-sm text-gray-400">Pending</div>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.15 }}
|
|
>
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-primary-blue">{stats.approved}</div>
|
|
<div className="text-sm text-gray-400">Approved</div>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.2 }}
|
|
>
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-white">${stats.totalInvestment.toLocaleString()}</div>
|
|
<div className="text-sm text-gray-400">Active Investment</div>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.25 }}
|
|
>
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-primary-blue">{(stats.totalImpressions / 1000).toFixed(0)}k</div>
|
|
<div className="text-sm text-gray-400">Impressions</div>
|
|
</Card>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-col lg:flex-row gap-4 mb-6">
|
|
{/* Search */}
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search sponsorships..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white placeholder-gray-500 focus:border-primary-blue focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Type Filter */}
|
|
<div className="flex items-center gap-2 overflow-x-auto pb-2 lg:pb-0">
|
|
{(['all', 'leagues', 'teams', 'drivers', 'races', 'platform'] as const).map((type) => {
|
|
const config = TYPE_CONFIG[type];
|
|
const Icon = config.icon;
|
|
const count = type === 'all' ? stats.total : statsByType[type];
|
|
return (
|
|
<button
|
|
key={type}
|
|
onClick={() => setTypeFilter(type)}
|
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors ${
|
|
typeFilter === type
|
|
? 'bg-primary-blue text-white'
|
|
: 'bg-iron-gray/50 text-gray-400 hover:bg-iron-gray'
|
|
}`}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{config.label}
|
|
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
|
typeFilter === type ? 'bg-white/20' : 'bg-charcoal-outline'
|
|
}`}>
|
|
{count}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Status Filter */}
|
|
<div className="flex items-center gap-2 overflow-x-auto">
|
|
{(['all', 'active', 'pending_approval', 'approved', 'rejected'] as const).map((status) => {
|
|
const config = status === 'all'
|
|
? { label: 'All', color: 'text-gray-400' }
|
|
: STATUS_CONFIG[status];
|
|
const count = status === 'all'
|
|
? stats.total
|
|
: MOCK_SPONSORSHIPS.filter(s => s.status === status).length;
|
|
return (
|
|
<button
|
|
key={status}
|
|
onClick={() => setStatusFilter(status)}
|
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
|
|
statusFilter === status
|
|
? 'bg-iron-gray text-white border border-charcoal-outline'
|
|
: 'text-gray-500 hover:text-gray-300'
|
|
}`}
|
|
>
|
|
{config.label}
|
|
{count > 0 && status !== 'all' && (
|
|
<span className={`ml-1.5 px-1.5 py-0.5 rounded text-xs ${
|
|
status === 'pending_approval' ? 'bg-warning-amber/20 text-warning-amber' : 'bg-charcoal-outline'
|
|
}`}>
|
|
{count}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sponsorship List */}
|
|
{filteredSponsorships.length === 0 ? (
|
|
<Card className="text-center py-16">
|
|
<Megaphone className="w-12 h-12 text-gray-600 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold text-white mb-2">No sponsorships found</h3>
|
|
<p className="text-gray-400 mb-6 max-w-md mx-auto">
|
|
{searchQuery || typeFilter !== 'all' || statusFilter !== 'all'
|
|
? 'Try adjusting your filters to see more results.'
|
|
: 'Start sponsoring leagues, teams, or drivers to grow your brand visibility.'}
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
|
<Link href="/leagues">
|
|
<Button variant="primary">
|
|
<Trophy className="w-4 h-4 mr-2" />
|
|
Browse Leagues
|
|
</Button>
|
|
</Link>
|
|
<Link href="/teams">
|
|
<Button variant="secondary">
|
|
<Users className="w-4 h-4 mr-2" />
|
|
Browse Teams
|
|
</Button>
|
|
</Link>
|
|
<Link href="/drivers">
|
|
<Button variant="secondary">
|
|
<Car className="w-4 h-4 mr-2" />
|
|
Browse Drivers
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{filteredSponsorships.map((sponsorship) => (
|
|
<SponsorshipCard key={sponsorship.id} sponsorship={sponsorship} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |