426 lines
16 KiB
TypeScript
426 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useMemo } from 'react';
|
|
import { motion, useReducedMotion } from 'framer-motion';
|
|
import Link from 'next/link';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import { siteConfig } from '@/lib/siteConfig';
|
|
import {
|
|
Trophy,
|
|
Users,
|
|
Eye,
|
|
Search,
|
|
Star,
|
|
ChevronRight,
|
|
Filter,
|
|
Car,
|
|
Flag,
|
|
TrendingUp,
|
|
CheckCircle2,
|
|
Clock,
|
|
Megaphone,
|
|
ArrowUpDown
|
|
} from 'lucide-react';
|
|
|
|
interface AvailableLeague {
|
|
id: string;
|
|
name: string;
|
|
game: string;
|
|
drivers: number;
|
|
avgViewsPerRace: number;
|
|
mainSponsorSlot: { available: boolean; price: number };
|
|
secondarySlots: { available: number; total: number; price: number };
|
|
rating: number;
|
|
tier: 'premium' | 'standard' | 'starter';
|
|
nextRace?: string;
|
|
seasonStatus: 'active' | 'upcoming' | 'completed';
|
|
description: string;
|
|
formattedAvgViews: string;
|
|
formattedCpm: string;
|
|
cpm: number;
|
|
tierConfig: any;
|
|
statusConfig: any;
|
|
}
|
|
|
|
type SortOption = 'rating' | 'drivers' | 'price' | 'views';
|
|
type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
|
|
type AvailabilityFilter = 'all' | 'main' | 'secondary';
|
|
|
|
interface SponsorLeaguesTemplateProps {
|
|
data: {
|
|
leagues: AvailableLeague[];
|
|
stats: {
|
|
total: number;
|
|
mainAvailable: number;
|
|
secondaryAvailable: number;
|
|
totalDrivers: number;
|
|
avgCpm: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
function LeagueCard({ league, index }: { league: AvailableLeague; index: number }) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
const tierConfig = {
|
|
premium: {
|
|
bg: 'bg-gradient-to-br from-yellow-500/10 to-amber-500/5',
|
|
border: 'border-yellow-500/30',
|
|
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
|
|
icon: '⭐'
|
|
},
|
|
standard: {
|
|
bg: 'bg-gradient-to-br from-primary-blue/10 to-cyan-500/5',
|
|
border: 'border-primary-blue/30',
|
|
badge: 'bg-primary-blue/20 text-primary-blue border-primary-blue/30',
|
|
icon: '🏆'
|
|
},
|
|
starter: {
|
|
bg: 'bg-gradient-to-br from-gray-500/10 to-slate-500/5',
|
|
border: 'border-gray-500/30',
|
|
badge: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
|
|
icon: '🚀'
|
|
},
|
|
};
|
|
|
|
const statusConfig = {
|
|
active: { color: 'text-performance-green', bg: 'bg-performance-green/10', label: 'Active Season' },
|
|
upcoming: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', label: 'Starting Soon' },
|
|
completed: { color: 'text-gray-400', bg: 'bg-gray-400/10', label: 'Season Ended' },
|
|
};
|
|
|
|
const config = league.tierConfig;
|
|
const status = league.statusConfig;
|
|
|
|
return (
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.05 }}
|
|
>
|
|
<Card className={`overflow-hidden border ${config.border} ${config.bg} hover:border-primary-blue/50 transition-all duration-300 h-full`}>
|
|
<div className="p-5">
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium capitalize border ${config.badge}`}>
|
|
{config.icon} {league.tier}
|
|
</span>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${status.bg} ${status.color}`}>
|
|
{status.label}
|
|
</span>
|
|
</div>
|
|
<h3 className="font-semibold text-white text-lg">{league.name}</h3>
|
|
<p className="text-sm text-gray-500">{league.game}</p>
|
|
</div>
|
|
<div className="flex items-center gap-1 bg-iron-gray/50 px-2 py-1 rounded">
|
|
<Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />
|
|
<span className="text-sm font-medium text-white">{league.rating}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<p className="text-sm text-gray-400 mb-4 line-clamp-2">{league.description}</p>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-3 gap-2 mb-4">
|
|
<div className="text-center p-2 bg-iron-gray/50 rounded-lg">
|
|
<div className="text-lg font-bold text-white">{league.drivers}</div>
|
|
<div className="text-xs text-gray-500">Drivers</div>
|
|
</div>
|
|
<div className="text-center p-2 bg-iron-gray/50 rounded-lg">
|
|
<div className="text-lg font-bold text-white">{league.formattedAvgViews}</div>
|
|
<div className="text-xs text-gray-500">Avg Views</div>
|
|
</div>
|
|
<div className="text-center p-2 bg-iron-gray/50 rounded-lg">
|
|
<div className="text-lg font-bold text-performance-green">{league.formattedCpm}</div>
|
|
<div className="text-xs text-gray-500">CPM</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Next Race */}
|
|
{league.nextRace && (
|
|
<div className="flex items-center gap-2 mb-4 text-sm">
|
|
<Clock className="w-4 h-4 text-gray-500" />
|
|
<span className="text-gray-400">Next:</span>
|
|
<span className="text-white">{league.nextRace}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Sponsorship Slots */}
|
|
<div className="space-y-2 mb-4">
|
|
<div className="flex items-center justify-between p-2.5 bg-iron-gray/30 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-2.5 h-2.5 rounded-full ${league.mainSponsorSlot.available ? 'bg-performance-green' : 'bg-racing-red'}`} />
|
|
<span className="text-sm text-gray-300">Main Sponsor</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
{league.mainSponsorSlot.available ? (
|
|
<span className="text-white font-semibold">${league.mainSponsorSlot.price}/season</span>
|
|
) : (
|
|
<span className="text-gray-500 flex items-center gap-1">
|
|
<CheckCircle2 className="w-3 h-3" /> Filled
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between p-2.5 bg-iron-gray/30 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-2.5 h-2.5 rounded-full ${league.secondarySlots.available > 0 ? 'bg-performance-green' : 'bg-racing-red'}`} />
|
|
<span className="text-sm text-gray-300">Secondary Slots</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
{league.secondarySlots.available > 0 ? (
|
|
<span className="text-white font-semibold">
|
|
{league.secondarySlots.available}/{league.secondarySlots.total} @ ${league.secondarySlots.price}
|
|
</span>
|
|
) : (
|
|
<span className="text-gray-500 flex items-center gap-1">
|
|
<CheckCircle2 className="w-3 h-3" /> Full
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-2">
|
|
<Link href={`/sponsor/leagues/${league.id}`} className="flex-1">
|
|
<Button variant="secondary" className="w-full text-sm">
|
|
View Details
|
|
</Button>
|
|
</Link>
|
|
{(league.mainSponsorSlot.available || league.secondarySlots.available > 0) && (
|
|
<Link href={`/sponsor/leagues/${league.id}?action=sponsor`} className="flex-1">
|
|
<Button variant="primary" className="w-full text-sm">
|
|
Sponsor
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export function SponsorLeaguesTemplate({ data }: SponsorLeaguesTemplateProps) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [tierFilter, setTierFilter] = useState<TierFilter>('all');
|
|
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>('all');
|
|
const [sortBy, setSortBy] = useState<SortOption>('rating');
|
|
|
|
// Filter and sort leagues
|
|
const filteredLeagues = data.leagues
|
|
.filter((league: any) => {
|
|
if (searchQuery && !league.name.toLowerCase().includes(searchQuery.toLowerCase())) {
|
|
return false;
|
|
}
|
|
if (tierFilter !== 'all' && league.tier !== tierFilter) {
|
|
return false;
|
|
}
|
|
if (availabilityFilter === 'main' && !league.mainSponsorSlot.available) {
|
|
return false;
|
|
}
|
|
if (availabilityFilter === 'secondary' && league.secondarySlots.available === 0) {
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.sort((a: any, b: any) => {
|
|
switch (sortBy) {
|
|
case 'rating': return b.rating - a.rating;
|
|
case 'drivers': return b.drivers - a.drivers;
|
|
case 'price': return a.mainSponsorSlot.price - b.mainSponsorSlot.price;
|
|
case 'views': return b.avgViewsPerRace - a.avgViewsPerRace;
|
|
default: return 0;
|
|
}
|
|
});
|
|
|
|
const stats = data.stats;
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto py-8 px-4">
|
|
{/* Breadcrumb */}
|
|
<div className="flex items-center gap-2 text-sm text-gray-400 mb-6">
|
|
<Link href="/sponsor/dashboard" className="hover:text-white transition-colors">Dashboard</Link>
|
|
<ChevronRight className="w-4 h-4" />
|
|
<span className="text-white">Browse Leagues</span>
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-bold text-white mb-2 flex items-center gap-3">
|
|
<Trophy className="w-7 h-7 text-primary-blue" />
|
|
League Sponsorship Marketplace
|
|
</h1>
|
|
<p className="text-gray-400">
|
|
Discover racing leagues looking for sponsors. All prices shown exclude VAT.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats Overview */}
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
>
|
|
<Card className="p-4 text-center">
|
|
<div className="text-2xl font-bold text-white">{stats.total}</div>
|
|
<div className="text-sm text-gray-400">Leagues</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 text-center">
|
|
<div className="text-2xl font-bold text-performance-green">{stats.mainAvailable}</div>
|
|
<div className="text-sm text-gray-400">Main Slots</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 text-center">
|
|
<div className="text-2xl font-bold text-primary-blue">{stats.secondaryAvailable}</div>
|
|
<div className="text-sm text-gray-400">Secondary Slots</div>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.3 }}
|
|
>
|
|
<Card className="p-4 text-center">
|
|
<div className="text-2xl font-bold text-white">{stats.totalDrivers}</div>
|
|
<div className="text-sm text-gray-400">Total Drivers</div>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.4 }}
|
|
>
|
|
<Card className="p-4 text-center">
|
|
<div className="text-2xl font-bold text-warning-amber">${stats.avgCpm}</div>
|
|
<div className="text-sm text-gray-400">Avg CPM</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 leagues..."
|
|
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>
|
|
|
|
{/* Tier Filter */}
|
|
<select
|
|
value={tierFilter}
|
|
onChange={(e) => setTierFilter(e.target.value as TierFilter)}
|
|
className="px-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white focus:border-primary-blue focus:outline-none"
|
|
>
|
|
<option value="all">All Tiers</option>
|
|
<option value="premium">⭐ Premium</option>
|
|
<option value="standard">🏆 Standard</option>
|
|
<option value="starter">🚀 Starter</option>
|
|
</select>
|
|
|
|
{/* Availability Filter */}
|
|
<select
|
|
value={availabilityFilter}
|
|
onChange={(e) => setAvailabilityFilter(e.target.value as AvailabilityFilter)}
|
|
className="px-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white focus:border-primary-blue focus:outline-none"
|
|
>
|
|
<option value="all">All Slots</option>
|
|
<option value="main">Main Available</option>
|
|
<option value="secondary">Secondary Available</option>
|
|
</select>
|
|
|
|
{/* Sort */}
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value as SortOption)}
|
|
className="px-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white focus:border-primary-blue focus:outline-none"
|
|
>
|
|
<option value="rating">Sort by Rating</option>
|
|
<option value="drivers">Sort by Drivers</option>
|
|
<option value="views">Sort by Views</option>
|
|
<option value="price">Sort by Price</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Results Count */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<p className="text-sm text-gray-400">
|
|
Showing {filteredLeagues.length} of {data.leagues.length} leagues
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<Link href="/teams">
|
|
<Button variant="secondary" className="text-sm">
|
|
<Users className="w-4 h-4 mr-2" />
|
|
Browse Teams
|
|
</Button>
|
|
</Link>
|
|
<Link href="/drivers">
|
|
<Button variant="secondary" className="text-sm">
|
|
<Car className="w-4 h-4 mr-2" />
|
|
Browse Drivers
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* League Grid */}
|
|
{filteredLeagues.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredLeagues.map((league: any, index: number) => (
|
|
<LeagueCard key={league.id} league={league} index={index} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Card className="text-center py-16">
|
|
<Trophy className="w-12 h-12 text-gray-500 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-white mb-2">No leagues found</h3>
|
|
<p className="text-gray-400 mb-6">Try adjusting your filters to see more results</p>
|
|
<Button variant="secondary" onClick={() => {
|
|
setSearchQuery('');
|
|
setTierFilter('all');
|
|
setAvailabilityFilter('all');
|
|
}}>
|
|
Clear Filters
|
|
</Button>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Platform Fee Notice */}
|
|
<div className="mt-8 rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Megaphone className="w-5 h-5 text-primary-blue flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p className="text-sm text-gray-300 font-medium mb-1">Platform Fee</p>
|
|
<p className="text-xs text-gray-500">
|
|
A {siteConfig.fees.platformFeePercent}% platform fee applies to all sponsorship payments. {siteConfig.fees.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |