Files
gridpilot.gg/apps/website/app/sponsor/campaigns/page.tsx
2026-01-12 01:01:49 +01:00

645 lines
25 KiB
TypeScript

'use client';
import { useState } 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 { useSponsorSponsorships } from "@/lib/hooks/sponsor/useSponsorSponsorships";
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
// ============================================================================
// ============================================================================
// 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: any }) {
const router = useRouter();
const shouldReduceMotion = useReducedMotion();
const typeConfig = TYPE_CONFIG[sponsorship.type as keyof typeof TYPE_CONFIG];
const statusConfig = STATUS_CONFIG[sponsorship.status as keyof typeof STATUS_CONFIG];
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.formattedImpressions}</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.formattedPrice}</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.periodDisplay}
</div>
<div className="flex items-center gap-1 text-gray-400">
<Trophy className="w-3.5 h-3.5" />
{sponsorship.formattedPrice}
</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 { data: sponsorshipsData, isLoading, error, retry } = useSponsorSponsorships('demo-sponsor-1');
if (isLoading) {
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>
);
}
if (error || !sponsorshipsData) {
return (
<div className="max-w-7xl mx-auto py-8 px-4 flex items-center justify-center min-h-[400px]">
<div className="text-center">
<p className="text-gray-400">{error?.getUserMessage() || 'No sponsorships data available'}</p>
{error && (
<Button variant="secondary" onClick={retry} className="mt-4">
Retry
</Button>
)}
</div>
</div>
);
}
const data = sponsorshipsData;
// Filter sponsorships
const filteredSponsorships = data.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: data.sponsorships.length,
active: data.sponsorships.filter(s => s.status === 'active').length,
pending: data.sponsorships.filter(s => s.status === 'pending_approval').length,
approved: data.sponsorships.filter(s => s.status === 'approved').length,
rejected: data.sponsorships.filter(s => s.status === 'rejected').length,
totalInvestment: data.sponsorships.filter(s => s.status === 'active').reduce((sum, s) => sum + s.price, 0),
totalImpressions: data.sponsorships.reduce((sum, s) => sum + s.impressions, 0),
};
// Stats by type
const statsByType = {
leagues: data.sponsorships.filter(s => s.type === 'leagues').length,
teams: data.sponsorships.filter(s => s.type === 'teams').length,
drivers: data.sponsorships.filter(s => s.type === 'drivers').length,
races: data.sponsorships.filter(s => s.type === 'races').length,
platform: data.sponsorships.filter(s => s.type === 'platform').length,
};
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
: data.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>
);
}