619 lines
26 KiB
TypeScript
619 lines
26 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useSearchParams } from 'next/navigation';
|
|
import { motion, useReducedMotion } from 'framer-motion';
|
|
import Link from 'next/link';
|
|
import { Card } from '@/ui/Card';
|
|
import { Button } from '@/ui/Button';
|
|
import { Box } from '@/ui/Box';
|
|
import { Stack } from '@/ui/Stack';
|
|
import { Text } from '@/ui/Text';
|
|
import { Heading } from '@/ui/Heading';
|
|
import { InfoBanner } from '@/ui/InfoBanner';
|
|
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
|
|
import {
|
|
Megaphone,
|
|
Trophy,
|
|
Users,
|
|
Eye,
|
|
Calendar,
|
|
ExternalLink,
|
|
Plus,
|
|
ChevronRight,
|
|
Check,
|
|
Clock,
|
|
XCircle,
|
|
Car,
|
|
Flag,
|
|
Search,
|
|
TrendingUp,
|
|
BarChart3,
|
|
ArrowUpRight,
|
|
ArrowDownRight,
|
|
Send,
|
|
ThumbsUp,
|
|
ThumbsDown,
|
|
RefreshCw,
|
|
} from 'lucide-react';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
type SponsorshipType = 'all' | 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
|
|
type SponsorshipStatus = 'all' | 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
|
|
|
|
// ============================================================================
|
|
// 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: unknown }) {
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
const s = sponsorship as any; // Temporary cast to avoid breaking logic
|
|
const typeConfig = TYPE_CONFIG[s.type as keyof typeof TYPE_CONFIG];
|
|
const statusConfig = STATUS_CONFIG[s.status as keyof typeof STATUS_CONFIG];
|
|
const TypeIcon = typeConfig.icon;
|
|
const StatusIcon = statusConfig.icon;
|
|
|
|
const daysRemaining = Math.ceil((s.endDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
const isExpiringSoon = daysRemaining > 0 && daysRemaining <= 30;
|
|
const isPending = s.status === 'pending_approval';
|
|
const isRejected = s.status === 'rejected';
|
|
const isApproved = s.status === 'approved';
|
|
|
|
const getEntityLink = () => {
|
|
switch (s.type) {
|
|
case 'leagues': return `/leagues/${s.entityId}`;
|
|
case 'teams': return `/teams/${s.entityId}`;
|
|
case 'drivers': return `/drivers/${s.entityId}`;
|
|
case 'races': return `/races/${s.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 */}
|
|
<Stack direction="row" align="start" justify="between" mb={4}>
|
|
<Stack direction="row" align="center" gap={3}>
|
|
<Box w="10" h="10" rounded="lg" bg={typeConfig.bgColor} display="flex" alignItems="center" justifyContent="center">
|
|
<TypeIcon className={`w-5 h-5 ${typeConfig.color}`} />
|
|
</Box>
|
|
<Box>
|
|
<Box display="flex" alignItems="center" gap={2} flexWrap="wrap">
|
|
<Text size="xs" weight="medium" px={2} py={0.5} rounded="md" bg={typeConfig.bgColor} color={typeConfig.color}>
|
|
{typeConfig.label}
|
|
</Text>
|
|
{s.tier && (
|
|
<Text size="xs" weight="medium" px={2} py={0.5} rounded="md" bg={s.tier === 'main' ? 'bg-primary-blue/20' : 'bg-purple-400/20'} color={s.tier === 'main' ? 'text-primary-blue' : 'text-purple-400'}>
|
|
{s.tier === 'main' ? 'Main Sponsor' : 'Secondary'}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Stack>
|
|
<Box display="flex" alignItems="center" gap={1} px={2.5} py={1} rounded="full" border bg={statusConfig.bgColor} color={statusConfig.color} borderColor={statusConfig.borderColor}>
|
|
<StatusIcon className="w-3 h-3" />
|
|
<Text size="xs" weight="medium">{statusConfig.label}</Text>
|
|
</Box>
|
|
</Stack>
|
|
|
|
{/* Entity Name */}
|
|
<Heading level={3} fontSize="lg" weight="semibold" color="text-white" mb={1}>{s.entityName}</Heading>
|
|
{s.details && (
|
|
<Text size="sm" color="text-gray-500" block mb={3}>{s.details}</Text>
|
|
)}
|
|
|
|
{/* Application/Approval Info for non-active states */}
|
|
{isPending && (
|
|
<Box mb={4} p={3} rounded="lg" bg="bg-warning-amber/5" border borderColor="border-warning-amber/20">
|
|
<Stack direction="row" align="center" gap={2} color="text-warning-amber" mb={2}>
|
|
<Send className="w-4 h-4" />
|
|
<Text size="sm" weight="medium">Application Pending</Text>
|
|
</Stack>
|
|
<Text size="xs" color="text-gray-400" block mb={2}>
|
|
Sent to <Text color="text-gray-300">{s.entityOwner}</Text> on{' '}
|
|
{s.applicationDate?.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
|
</Text>
|
|
{s.applicationMessage && (
|
|
<Text size="xs" color="text-gray-500" italic block>"{s.applicationMessage}"</Text>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{isApproved && (
|
|
<Box mb={4} p={3} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
|
|
<Stack direction="row" align="center" gap={2} color="text-primary-blue" mb={1}>
|
|
<ThumbsUp className="w-4 h-4" />
|
|
<Text size="sm" weight="medium">Approved!</Text>
|
|
</Stack>
|
|
<Text size="xs" color="text-gray-400" block>
|
|
Approved by <Text color="text-gray-300">{s.entityOwner}</Text> on{' '}
|
|
{s.approvalDate?.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
|
</Text>
|
|
<Text size="xs" color="text-gray-500" block mt={1}>
|
|
Starts {s.startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
|
|
{isRejected && (
|
|
<Box mb={4} p={3} rounded="lg" bg="bg-racing-red/5" border borderColor="border-racing-red/20">
|
|
<Stack direction="row" align="center" gap={2} color="text-racing-red" mb={1}>
|
|
<ThumbsDown className="w-4 h-4" />
|
|
<Text size="sm" weight="medium">Application Declined</Text>
|
|
</Stack>
|
|
{s.rejectionReason && (
|
|
<Text size="xs" color="text-gray-400" block mt={1}>
|
|
Reason: <Text color="text-gray-300">{s.rejectionReason}</Text>
|
|
</Text>
|
|
)}
|
|
<Button variant="secondary" className="mt-2 text-xs">
|
|
<RefreshCw className="w-3 h-3 mr-1" />
|
|
Reapply
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Metrics Grid - Only show for active sponsorships */}
|
|
{s.status === 'active' && (
|
|
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={3} mb={4}>
|
|
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
|
|
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
|
|
<Eye className="w-3 h-3" />
|
|
<Text size="xs">Impressions</Text>
|
|
</Box>
|
|
<Stack direction="row" align="center" gap={2}>
|
|
<Text color="text-white" weight="semibold">{s.formattedImpressions}</Text>
|
|
{s.impressionsChange !== undefined && s.impressionsChange !== 0 && (
|
|
<Text size="xs" display="flex" alignItems="center" color={s.impressionsChange > 0 ? 'text-performance-green' : 'text-racing-red'}>
|
|
{s.impressionsChange > 0 ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
|
|
{Math.abs(s.impressionsChange)}%
|
|
</Text>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
|
|
{s.engagement && (
|
|
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
|
|
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
|
|
<TrendingUp className="w-3 h-3" />
|
|
<Text size="xs">Engagement</Text>
|
|
</Box>
|
|
<Text color="text-white" weight="semibold">{s.engagement}%</Text>
|
|
</Box>
|
|
)}
|
|
|
|
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
|
|
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
|
|
<Calendar className="w-3 h-3" />
|
|
<Text size="xs">Period</Text>
|
|
</Box>
|
|
<Text color="text-white" weight="semibold" size="xs">
|
|
{s.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - {s.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
|
</Text>
|
|
</Box>
|
|
|
|
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
|
|
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
|
|
<Trophy className="w-3 h-3" />
|
|
<Text size="xs">Investment</Text>
|
|
</Box>
|
|
<Text color="text-white" weight="semibold">{s.formattedPrice}</Text>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Basic info for non-active */}
|
|
{s.status !== 'active' && (
|
|
<Stack direction="row" align="center" gap={4} mb={4}>
|
|
<Box display="flex" alignItems="center" gap={1} color="text-gray-400">
|
|
<Calendar className="w-3.5 h-3.5" />
|
|
<Text size="sm">{s.periodDisplay}</Text>
|
|
</Box>
|
|
<Box display="flex" alignItems="center" gap={1} color="text-gray-400">
|
|
<Trophy className="w-3.5 h-3.5" />
|
|
<Text size="sm">{s.formattedPrice}</Text>
|
|
</Box>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-charcoal-outline/50">
|
|
<Box display="flex" alignItems="center" gap={2}>
|
|
{s.status === 'active' && (
|
|
<Text size="xs" color={isExpiringSoon ? 'text-warning-amber' : 'text-gray-500'}>
|
|
{daysRemaining > 0 ? `${daysRemaining} days remaining` : 'Ended'}
|
|
</Text>
|
|
)}
|
|
{isPending && (
|
|
<Text size="xs" color="text-gray-500">
|
|
Waiting for response...
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
<Stack direction="row" align="center" gap={2}>
|
|
{s.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>
|
|
)}
|
|
{s.status === 'active' && (
|
|
<Button variant="secondary" className="text-xs">
|
|
Details
|
|
<ChevronRight className="w-3 h-3 ml-1" />
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Component
|
|
// ============================================================================
|
|
|
|
export default function SponsorCampaignsPage() {
|
|
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: unknown) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const sponsorship = s as any;
|
|
if (typeFilter !== 'all' && sponsorship.type !== typeFilter) return false;
|
|
if (statusFilter !== 'all' && sponsorship.status !== statusFilter) return false;
|
|
if (searchQuery && !sponsorship.entityName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
|
return true;
|
|
});
|
|
|
|
// Calculate stats
|
|
const stats = {
|
|
total: data.sponsorships.length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
active: data.sponsorships.filter((s: any) => s.status === 'active').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
pending: data.sponsorships.filter((s: any) => s.status === 'pending_approval').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
approved: data.sponsorships.filter((s: any) => s.status === 'approved').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
rejected: data.sponsorships.filter((s: any) => s.status === 'rejected').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
totalInvestment: data.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0),
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
totalImpressions: data.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0),
|
|
};
|
|
|
|
// Stats by type
|
|
const statsByType = {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
leagues: data.sponsorships.filter((s: any) => s.type === 'leagues').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
teams: data.sponsorships.filter((s: any) => s.type === 'teams').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
drivers: data.sponsorships.filter((s: any) => s.type === 'drivers').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
races: data.sponsorships.filter((s: any) => s.type === 'races').length,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
platform: data.sponsorships.filter((s: any) => s.type === 'platform').length,
|
|
};
|
|
|
|
return (
|
|
<Box maxWidth="7xl" mx="auto" py={8} px={4}>
|
|
{/* Header */}
|
|
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4} mb={8}>
|
|
<Box>
|
|
<Heading level={1} fontSize="2xl" weight="bold" color="text-white" icon={<Megaphone className="w-7 h-7 text-primary-blue" />}>
|
|
My Sponsorships
|
|
</Heading>
|
|
<Text color="text-gray-400" mt={1} block>Manage applications and active sponsorship campaigns</Text>
|
|
</Box>
|
|
<Box display="flex" alignItems="center" gap={3}>
|
|
<Link href="/leagues">
|
|
<Button variant="primary">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Find Opportunities
|
|
</Button>
|
|
</Link>
|
|
</Box>
|
|
</Stack>
|
|
|
|
{/* 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">
|
|
<Text size="sm">
|
|
You have <Text weight="bold" color="text-white">{stats.pending} pending application{stats.pending !== 1 ? 's' : ''}</Text> waiting for approval.
|
|
League admins, team owners, and drivers review applications before accepting sponsorships.
|
|
</Text>
|
|
</InfoBanner>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Quick Stats */}
|
|
<Box display="grid" gridCols={{ base: 2, md: 6 }} gap={4} mb={8}>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
>
|
|
<Card className="p-4">
|
|
<Text size="2xl" weight="bold" color="text-white" block>{stats.total}</Text>
|
|
<Text size="sm" color="text-gray-400">Total</Text>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.05 }}
|
|
>
|
|
<Card className="p-4">
|
|
<Text size="2xl" weight="bold" color="text-performance-green" block>{stats.active}</Text>
|
|
<Text size="sm" color="text-gray-400">Active</Text>
|
|
</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' : ''}`}>
|
|
<Text size="2xl" weight="bold" color="text-warning-amber" block>{stats.pending}</Text>
|
|
<Text size="sm" color="text-gray-400">Pending</Text>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.15 }}
|
|
>
|
|
<Card className="p-4">
|
|
<Text size="2xl" weight="bold" color="text-primary-blue" block>{stats.approved}</Text>
|
|
<Text size="sm" color="text-gray-400">Approved</Text>
|
|
</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 size="2xl" weight="bold" color="text-white" block>${stats.totalInvestment.toLocaleString()}</Text>
|
|
<Text size="sm" color="text-gray-400">Active Investment</Text>
|
|
</Card>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.25 }}
|
|
>
|
|
<Card className="p-4">
|
|
<Text size="2xl" weight="bold" color="text-primary-blue" block>{(stats.totalImpressions / 1000).toFixed(0)}k</Text>
|
|
<Text size="sm" color="text-gray-400">Impressions</Text>
|
|
</Card>
|
|
</motion.div>
|
|
</Box>
|
|
|
|
{/* Filters */}
|
|
<Stack direction={{ base: 'col', lg: 'row' }} gap={4} mb={6}>
|
|
{/* Search */}
|
|
<Box position="relative" flexGrow={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"
|
|
/>
|
|
</Box>
|
|
|
|
{/* Type Filter */}
|
|
<Box display="flex" alignItems="center" gap={2} overflow="auto" pb={{ base: 2, lg: 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'
|
|
} border-0 cursor-pointer`}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{config.label}
|
|
<Text size="xs" px={1.5} py={0.5} rounded="sm" bg={typeFilter === type ? 'bg-white/20' : 'bg-charcoal-outline'}>
|
|
{count}
|
|
</Text>
|
|
</button>
|
|
);
|
|
})}
|
|
</Box>
|
|
|
|
{/* Status Filter */}
|
|
<Box display="flex" alignItems="center" gap={2} overflow="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: any) => 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'
|
|
} border-0 cursor-pointer`}
|
|
>
|
|
{config.label}
|
|
{count > 0 && status !== 'all' && (
|
|
<Text size="xs" ml={1.5} px={1.5} py={0.5} rounded="sm" bg={status === 'pending_approval' ? 'bg-warning-amber/20' : 'bg-charcoal-outline'} color={status === 'pending_approval' ? 'text-warning-amber' : ''}>
|
|
{count}
|
|
</Text>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</Box>
|
|
</Stack>
|
|
|
|
{/* Sponsorship List */}
|
|
{filteredSponsorships.length === 0 ? (
|
|
<Card className="text-center py-16">
|
|
<Megaphone className="w-12 h-12 text-gray-600 mx-auto mb-4" />
|
|
<Heading level={3} fontSize="lg" weight="semibold" color="text-white" mb={2}>No sponsorships found</Heading>
|
|
<Text color="text-gray-400" mb={6} maxWidth="md" mx="auto" block>
|
|
{searchQuery || typeFilter !== 'all' || statusFilter !== 'all'
|
|
? 'Try adjusting your filters to see more results.'
|
|
: 'Start sponsoring leagues, teams, or drivers to grow your brand visibility.'}
|
|
</Text>
|
|
<Stack direction={{ base: 'col', md: '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>
|
|
</Stack>
|
|
</Card>
|
|
) : (
|
|
<Box display="grid" gridCols={{ base: 1, lg: 2 }} gap={4}>
|
|
{filteredSponsorships.map((sponsorship: any) => (
|
|
<SponsorshipCard key={sponsorship.id} sponsorship={sponsorship} />
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
} |