di usage in website

This commit is contained in:
2026-01-06 19:36:03 +01:00
parent 589b55a87e
commit e589c30bf8
191 changed files with 6367 additions and 4253 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
@@ -10,8 +10,9 @@ import StatusBadge from '@/components/ui/StatusBadge';
import InfoBanner from '@/components/ui/InfoBanner';
import PageHeader from '@/components/ui/PageHeader';
import { siteConfig } from '@/lib/siteConfig';
import { BillingViewModel } from '@/lib/view-models/BillingViewModel';
import { useServices } from '@/lib/services/ServiceProvider';
import { useSponsorBilling } from '@/hooks/sponsor/useSponsorBilling';
import { useInject } from '@/lib/di/hooks/useInject';
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
import {
CreditCard,
DollarSign,
@@ -260,29 +261,12 @@ function InvoiceRow({ invoice, index }: { invoice: any; index: number }) {
export default function SponsorBillingPage() {
const shouldReduceMotion = useReducedMotion();
const { sponsorService } = useServices();
const [data, setData] = useState<BillingViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const sponsorService = useInject(SPONSOR_SERVICE_TOKEN);
const [showAllInvoices, setShowAllInvoices] = useState(false);
useEffect(() => {
const loadBilling = async () => {
try {
const billingData = await sponsorService.getBilling('demo-sponsor-1');
setData(new BillingViewModel(billingData));
} catch (err) {
console.error('Error loading billing data:', err);
setError('Failed to load billing data');
} finally {
setLoading(false);
}
};
const { data: billingData, isLoading, error, retry } = useSponsorBilling('demo-sponsor-1');
loadBilling();
}, []);
if (loading) {
if (isLoading) {
return (
<div className="max-w-5xl mx-auto py-8 px-4 flex items-center justify-center min-h-[400px]">
<div className="text-center">
@@ -293,16 +277,23 @@ export default function SponsorBillingPage() {
);
}
if (error || !data) {
if (error || !billingData) {
return (
<div className="max-w-5xl mx-auto py-8 px-4 flex items-center justify-center min-h-[400px]">
<div className="text-center">
<p className="text-gray-400">{error || 'No billing data available'}</p>
<p className="text-gray-400">{error?.getUserMessage() || 'No billing data available'}</p>
{error && (
<Button variant="secondary" onClick={retry} className="mt-4">
Retry
</Button>
)}
</div>
</div>
);
}
const data = billingData;
const handleSetDefault = (methodId: string) => {
// In a real app, this would call an API
console.log('Setting default payment method:', methodId);

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { motion, useReducedMotion, AnimatePresence } from 'framer-motion';
import Link from 'next/link';
@@ -8,13 +8,12 @@ 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 { useServices } from '@/lib/services/ServiceProvider';
import { SponsorSponsorshipsViewModel } from '@/lib/view-models/SponsorSponsorshipsViewModel';
import {
Megaphone,
Trophy,
Users,
Eye,
import { useSponsorSponsorships } from '@/hooks/sponsor/useSponsorSponsorships';
import {
Megaphone,
Trophy,
Users,
Eye,
Calendar,
ExternalLink,
Plus,
@@ -364,37 +363,15 @@ export default function SponsorCampaignsPage() {
const router = useRouter();
const searchParams = useSearchParams();
const shouldReduceMotion = useReducedMotion();
const { sponsorService } = useServices();
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, setData] = useState<SponsorSponsorshipsViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadSponsorships = async () => {
try {
const sponsorshipsData = await sponsorService.getSponsorSponsorships('demo-sponsor-1');
if (sponsorshipsData) {
setData(sponsorshipsData);
} else {
setError('Failed to load sponsorships data');
}
} catch (err) {
console.error('Error loading sponsorships:', err);
setError('Failed to load sponsorships data');
} finally {
setLoading(false);
}
};
const { data: sponsorshipsData, isLoading, error, retry } = useSponsorSponsorships('demo-sponsor-1');
loadSponsorships();
}, []);
if (loading) {
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">
@@ -405,16 +382,23 @@ export default function SponsorCampaignsPage() {
);
}
if (error || !data) {
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 || 'No sponsorships data available'}</p>
<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;
@@ -443,17 +427,6 @@ export default function SponsorCampaignsPage() {
platform: data.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 */}

View File

@@ -2,7 +2,6 @@
export const dynamic = 'force-dynamic';
import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { motion, useReducedMotion, AnimatePresence } from 'framer-motion';
import Card from '@/components/ui/Card';
@@ -38,69 +37,46 @@ import {
RefreshCw
} from 'lucide-react';
import Link from 'next/link';
import { useServices } from '@/lib/services/ServiceProvider';
import { SponsorDashboardViewModel } from '@/lib/view-models/SponsorDashboardViewModel';
import { useInject } from '@/lib/di/hooks/useInject';
import { SPONSOR_SERVICE_TOKEN, POLICY_SERVICE_TOKEN } from '@/lib/di/tokens';
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
export default function SponsorDashboardPage() {
const shouldReduceMotion = useReducedMotion();
const { sponsorService, policyService } = useServices();
const [timeRange, setTimeRange] = useState<'7d' | '30d' | '90d' | 'all'>('30d');
const [loading, setLoading] = useState(true);
const [data, setData] = useState<SponsorDashboardViewModel | null>(null);
const [error, setError] = useState<string | null>(null);
const sponsorService = useInject(SPONSOR_SERVICE_TOKEN);
const policyService = useInject(POLICY_SERVICE_TOKEN);
const {
data: policySnapshot,
isLoading: policyLoading,
isError: policyError,
} = useQuery({
const policyQuery = useQuery({
queryKey: ['policySnapshot'],
queryFn: () => policyService.getSnapshot(),
staleTime: 60_000,
gcTime: 5 * 60_000,
});
const enhancedPolicyQuery = enhanceQueryResult(policyQuery);
const policySnapshot = enhancedPolicyQuery.data;
const policyLoading = enhancedPolicyQuery.isLoading;
const policyError = enhancedPolicyQuery.error;
const sponsorPortalState = policySnapshot
? policyService.getCapabilityState(policySnapshot, 'sponsors.portal')
: null;
useEffect(() => {
if (policyLoading) {
return;
}
const dashboardQuery = useQuery({
queryKey: ['sponsorDashboard', 'demo-sponsor-1', sponsorPortalState],
queryFn: () => sponsorService.getSponsorDashboard('demo-sponsor-1'),
enabled: !!policySnapshot && sponsorPortalState === 'enabled',
staleTime: 300_000,
gcTime: 10 * 60_000,
});
if (policyError || sponsorPortalState !== 'enabled') {
setError(
sponsorPortalState === 'coming_soon'
? 'Sponsor portal is coming soon.'
: 'Sponsor portal is currently unavailable.',
);
setLoading(false);
return;
}
const enhancedDashboardQuery = enhanceQueryResult(dashboardQuery);
const dashboardData = enhancedDashboardQuery.data;
const dashboardLoading = enhancedDashboardQuery.isLoading;
const dashboardError = enhancedDashboardQuery.error;
const loadDashboard = async () => {
try {
const dashboardData = await sponsorService.getSponsorDashboard('demo-sponsor-1');
if (dashboardData) {
setData(dashboardData);
} else {
setError('Failed to load dashboard data');
}
} catch (err) {
console.error('Error loading dashboard:', err);
setError('Failed to load dashboard data');
} finally {
setLoading(false);
}
};
void loadDashboard();
}, [policyLoading, policyError, sponsorPortalState, sponsorService]);
const loading = policyLoading || dashboardLoading;
const error = policyError || dashboardError || (sponsorPortalState !== 'enabled' && sponsorPortalState !== null);
if (loading) {
return (
@@ -113,36 +89,31 @@ export default function SponsorDashboardPage() {
);
}
if (error || !data) {
if (error || !dashboardData) {
const errorMessage = sponsorPortalState === 'coming_soon'
? 'Sponsor portal is coming soon.'
: sponsorPortalState === 'disabled'
? 'Sponsor portal is currently unavailable.'
: 'Failed to load dashboard data';
return (
<div className="max-w-7xl mx-auto py-8 px-4 flex items-center justify-center min-h-[600px]">
<div className="text-center">
<p className="text-gray-400">{error || 'No dashboard data available'}</p>
<p className="text-gray-400">{errorMessage}</p>
</div>
</div>
);
}
const categoryData = data.categoryData;
if (loading) {
return (
<div className="max-w-7xl mx-auto py-8 px-4 flex items-center justify-center min-h-[600px]">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin text-primary-blue mx-auto mb-4" />
<p className="text-gray-400">Loading dashboard...</p>
</div>
</div>
);
}
const categoryData = dashboardData.categoryData;
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">Sponsor Dashboard</h1>
<p className="text-gray-400">Welcome back, {data.sponsorName}</p>
<h2 className="text-2xl font-bold text-white">Sponsor Dashboard</h2>
<p className="text-gray-400">Welcome back, {dashboardData.sponsorName}</p>
</div>
<div className="flex items-center gap-3">
{/* Time Range Selector */}
@@ -150,9 +121,9 @@ export default function SponsorDashboardPage() {
{(['7d', '30d', '90d', 'all'] as const).map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
onClick={() => {}}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
timeRange === range
false
? 'bg-primary-blue text-white'
: 'text-gray-400 hover:text-white'
}`}
@@ -178,29 +149,29 @@ export default function SponsorDashboardPage() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<MetricCard
title="Total Impressions"
value={data.totalImpressions}
change={data.metrics.impressionsChange}
value={dashboardData.totalImpressions}
change={dashboardData.metrics.impressionsChange}
icon={Eye}
delay={0}
/>
<MetricCard
title="Unique Viewers"
value={data.metrics.uniqueViewers}
change={data.metrics.viewersChange}
value={dashboardData.metrics.uniqueViewers}
change={dashboardData.metrics.viewersChange}
icon={Users}
delay={0.1}
/>
<MetricCard
title="Engagement Rate"
value={data.metrics.exposure}
change={data.metrics.exposureChange}
value={dashboardData.metrics.exposure}
change={dashboardData.metrics.exposureChange}
icon={TrendingUp}
suffix="%"
delay={0.2}
/>
<MetricCard
title="Total Investment"
value={data.totalInvestment}
value={dashboardData.totalInvestment}
icon={DollarSign}
prefix="$"
delay={0.3}
@@ -210,7 +181,7 @@ export default function SponsorDashboardPage() {
{/* Sponsorship Categories */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">Your Sponsorships</h2>
<h3 className="text-lg font-semibold text-white">Your Sponsorships</h3>
<Link href="/sponsor/campaigns">
<Button variant="secondary" className="text-sm">
View All
@@ -270,7 +241,7 @@ export default function SponsorDashboardPage() {
{/* Top Performing Sponsorships */}
<Card>
<div className="flex items-center justify-between p-4 border-b border-charcoal-outline">
<h2 className="text-lg font-semibold text-white">Top Performing</h2>
<h3 className="text-lg font-semibold text-white">Top Performing</h3>
<Link href="/leagues">
<Button variant="secondary" className="text-sm">
<Plus className="w-4 h-4 mr-1" />
@@ -280,7 +251,7 @@ export default function SponsorDashboardPage() {
</div>
<div className="divide-y divide-charcoal-outline/50">
{/* Leagues */}
{data.sponsorships.leagues.map((league) => (
{dashboardData.sponsorships.leagues.map((league: any) => (
<div key={league.id} className="flex items-center justify-between p-4 hover:bg-iron-gray/30 transition-colors">
<div className="flex items-center gap-4">
<div className={`px-2 py-1 rounded text-xs font-medium ${
@@ -313,7 +284,7 @@ export default function SponsorDashboardPage() {
))}
{/* Teams */}
{data.sponsorships.teams.map((team) => (
{dashboardData.sponsorships.teams.map((team: any) => (
<div key={team.id} className="flex items-center justify-between p-4 hover:bg-iron-gray/30 transition-colors">
<div className="flex items-center gap-4">
<div className="px-2 py-1 rounded text-xs font-medium bg-purple-500/20 text-purple-400 border border-purple-500/30">
@@ -340,7 +311,7 @@ export default function SponsorDashboardPage() {
))}
{/* Drivers */}
{data.sponsorships.drivers.slice(0, 2).map((driver) => (
{dashboardData.sponsorships.drivers.slice(0, 2).map((driver: any) => (
<div key={driver.id} className="flex items-center justify-between p-4 hover:bg-iron-gray/30 transition-colors">
<div className="flex items-center gap-4">
<div className="px-2 py-1 rounded text-xs font-medium bg-performance-green/20 text-performance-green border border-performance-green/30">
@@ -371,15 +342,15 @@ export default function SponsorDashboardPage() {
{/* Upcoming Events */}
<Card>
<div className="p-4 border-b border-charcoal-outline">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Calendar className="w-5 h-5 text-warning-amber" />
Upcoming Sponsored Events
</h2>
</h3>
</div>
<div className="p-4">
{data.sponsorships.races.length > 0 ? (
{dashboardData.sponsorships.races.length > 0 ? (
<div className="space-y-3">
{data.sponsorships.races.map((race) => (
{dashboardData.sponsorships.races.map((race: any) => (
<div key={race.id} className="flex items-center justify-between p-3 rounded-lg bg-iron-gray/30">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-warning-amber/10 flex items-center justify-center">
@@ -448,14 +419,14 @@ export default function SponsorDashboardPage() {
</Card>
{/* Renewal Alerts */}
{data.upcomingRenewals.length > 0 && (
{dashboardData.upcomingRenewals.length > 0 && (
<Card className="p-4">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Bell className="w-5 h-5 text-warning-amber" />
Upcoming Renewals
</h3>
<div className="space-y-3">
{data.upcomingRenewals.map((renewal) => (
{dashboardData.upcomingRenewals.map((renewal: any) => (
<RenewalAlert key={renewal.id} renewal={renewal} />
))}
</div>
@@ -466,7 +437,7 @@ export default function SponsorDashboardPage() {
<Card className="p-4">
<h3 className="text-lg font-semibold text-white mb-4">Recent Activity</h3>
<div>
{data.recentActivity.map((activity) => (
{dashboardData.recentActivity.map((activity: any) => (
<ActivityItem key={activity.id} activity={activity} />
))}
</div>
@@ -481,16 +452,16 @@ export default function SponsorDashboardPage() {
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-gray-400">Active Sponsorships</span>
<span className="font-medium text-white">{data.activeSponsorships}</span>
<span className="font-medium text-white">{dashboardData.activeSponsorships}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400">Total Investment</span>
<span className="font-medium text-white">{data.formattedTotalInvestment}</span>
<span className="font-medium text-white">{dashboardData.formattedTotalInvestment}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400">Cost per 1K Views</span>
<span className="font-medium text-performance-green">
{data.costPerThousandViews}
{dashboardData.costPerThousandViews}
</span>
</div>
<div className="flex items-center justify-between">

View File

@@ -0,0 +1,439 @@
'use client';
import { useState } 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 { useAvailableLeagues } from '@/hooks/sponsor/useAvailableLeagues';
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;
}
type SortOption = 'rating' | 'drivers' | 'price' | 'views';
type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
type AvailabilityFilter = 'all' | 'main' | 'secondary';
function LeagueCard({ league, index }: { league: any; 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 default function SponsorLeaguesInteractive() {
const shouldReduceMotion = useReducedMotion();
const { data, isLoading, isError, error } = useAvailableLeagues();
const [searchQuery, setSearchQuery] = useState('');
const [tierFilter, setTierFilter] = useState<TierFilter>('all');
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>('all');
const [sortBy, setSortBy] = useState<SortOption>('rating');
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 leagues...</p>
</div>
</div>
);
}
if (isError || !data) {
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?.message || 'No leagues data available'}</p>
</div>
</div>
);
}
// Filter and sort leagues
const filteredLeagues = data.leagues
.filter(league => {
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, b) => {
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;
}
});
// Calculate summary stats
const stats = {
total: data.leagues.length,
mainAvailable: data.leagues.filter(l => l.mainSponsorSlot.available).length,
secondaryAvailable: data.leagues.reduce((sum, l) => sum + l.secondarySlots.available, 0),
totalDrivers: data.leagues.reduce((sum, l) => sum + l.drivers, 0),
avgCpm: Math.round(
data.leagues.reduce((sum, l) => sum + l.cpm, 0) / data.leagues.length
),
};
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, index) => (
<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>
);
}

View File

@@ -1,14 +1,13 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { useParams, useSearchParams } from 'next/navigation';
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 { LeagueDetailViewModel } from '@/lib/view-models/LeagueDetailViewModel';
import { useServices } from '@/lib/services/ServiceProvider';
import { useSponsorLeagueDetail } from '@/hooks/sponsor/useSponsorLeagueDetail';
import {
Trophy,
Users,
@@ -39,36 +38,14 @@ export default function SponsorLeagueDetailPage() {
const searchParams = useSearchParams();
const shouldReduceMotion = useReducedMotion();
const { sponsorService } = useServices();
const leagueId = params.id as string;
const showSponsorAction = searchParams.get('action') === 'sponsor';
const [activeTab, setActiveTab] = useState<TabType>(showSponsorAction ? 'sponsor' : 'overview');
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');
const [data, setData] = useState<LeagueDetailViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const leagueId = params.id as string;
const { data: leagueData, isLoading, error, retry } = useSponsorLeagueDetail(leagueId);
useEffect(() => {
const loadLeagueDetail = async () => {
try {
const leagueData = await sponsorService.getLeagueDetail(leagueId);
setData(new LeagueDetailViewModel(leagueData));
} catch (err) {
console.error('Error loading league detail:', err);
setError('Failed to load league detail');
} finally {
setLoading(false);
}
};
if (leagueId) {
loadLeagueDetail();
}
}, [leagueId, sponsorService]);
if (loading) {
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">
@@ -79,16 +56,22 @@ export default function SponsorLeagueDetailPage() {
);
}
if (error || !data) {
if (error || !leagueData) {
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 || 'No league data available'}</p>
<p className="text-gray-400">{error?.getUserMessage() || 'No league data available'}</p>
{error && (
<Button variant="secondary" onClick={retry} className="mt-4">
Retry
</Button>
)}
</div>
</div>
);
}
const data = leagueData;
const league = data.league;
const config = league.tierConfig;

View File

@@ -1,460 +1,3 @@
'use client';
import SponsorLeaguesInteractive from './SponsorLeaguesInteractive';
import { useState, useEffect } 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 { AvailableLeaguesViewModel } from '@/lib/view-models/AvailableLeaguesViewModel';
import { useServices } from '@/lib/services/ServiceProvider';
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;
}
type SortOption = 'rating' | 'drivers' | 'price' | 'views';
type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
type AvailabilityFilter = 'all' | 'main' | 'secondary';
function LeagueCard({ league, index }: { league: any; 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 default function SponsorLeaguesPage() {
const shouldReduceMotion = useReducedMotion();
const { sponsorService } = useServices();
const [searchQuery, setSearchQuery] = useState('');
const [tierFilter, setTierFilter] = useState<TierFilter>('all');
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>('all');
const [sortBy, setSortBy] = useState<SortOption>('rating');
const [data, setData] = useState<AvailableLeaguesViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadLeagues = async () => {
try {
const leaguesData = await sponsorService.getAvailableLeagues();
setData(new AvailableLeaguesViewModel(leaguesData));
} catch (err) {
console.error('Error loading leagues:', err);
setError('Failed to load leagues data');
} finally {
setLoading(false);
}
};
loadLeagues();
}, [sponsorService]);
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 leagues...</p>
</div>
</div>
);
}
if (error || !data) {
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 || 'No leagues data available'}</p>
</div>
</div>
);
}
// Filter and sort leagues
const filteredLeagues = data.leagues
.filter(league => {
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, b) => {
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;
}
});
// Calculate summary stats
const stats = {
total: data.leagues.length,
mainAvailable: data.leagues.filter(l => l.mainSponsorSlot.available).length,
secondaryAvailable: data.leagues.reduce((sum, l) => sum + l.secondarySlots.available, 0),
totalDrivers: data.leagues.reduce((sum, l) => sum + l.drivers, 0),
avgCpm: Math.round(
data.leagues.reduce((sum, l) => sum + l.cpm, 0) / data.leagues.length
),
};
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, index) => (
<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>
);
}
export default SponsorLeaguesInteractive;