335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import {
|
|
Megaphone,
|
|
Trophy,
|
|
Users,
|
|
Eye,
|
|
Calendar,
|
|
ExternalLink,
|
|
Plus,
|
|
ChevronRight,
|
|
Check,
|
|
Clock,
|
|
XCircle
|
|
} from 'lucide-react';
|
|
|
|
interface Sponsorship {
|
|
id: string;
|
|
leagueId: string;
|
|
leagueName: string;
|
|
tier: 'main' | 'secondary';
|
|
status: 'active' | 'pending' | 'expired';
|
|
startDate: Date;
|
|
endDate: Date;
|
|
price: number;
|
|
impressions: number;
|
|
drivers: number;
|
|
}
|
|
|
|
interface SponsorshipDetailApi {
|
|
id: string;
|
|
leagueId: string;
|
|
leagueName: string;
|
|
seasonId: string;
|
|
seasonName: string;
|
|
seasonStartDate?: string;
|
|
seasonEndDate?: string;
|
|
tier: 'main' | 'secondary';
|
|
status: string;
|
|
pricing: {
|
|
amount: number;
|
|
currency: string;
|
|
};
|
|
metrics: {
|
|
drivers: number;
|
|
races: number;
|
|
completedRaces: number;
|
|
impressions: number;
|
|
};
|
|
createdAt: string;
|
|
activatedAt?: string;
|
|
}
|
|
|
|
interface SponsorSponsorshipsResponse {
|
|
sponsorId: string;
|
|
sponsorName: string;
|
|
sponsorships: SponsorshipDetailApi[];
|
|
summary: {
|
|
totalSponsorships: number;
|
|
activeSponsorships: number;
|
|
totalInvestment: number;
|
|
totalPlatformFees: number;
|
|
currency: string;
|
|
};
|
|
}
|
|
|
|
function mapSponsorshipStatus(status: string): 'active' | 'pending' | 'expired' {
|
|
switch (status) {
|
|
case 'active':
|
|
return 'active';
|
|
case 'pending':
|
|
return 'pending';
|
|
default:
|
|
return 'expired';
|
|
}
|
|
}
|
|
|
|
function mapApiToSponsorships(response: SponsorSponsorshipsResponse): Sponsorship[] {
|
|
return response.sponsorships.map((s) => {
|
|
const start = s.seasonStartDate ? new Date(s.seasonStartDate) : new Date(s.createdAt);
|
|
const end = s.seasonEndDate ? new Date(s.seasonEndDate) : start;
|
|
|
|
return {
|
|
id: s.id,
|
|
leagueId: s.leagueId,
|
|
leagueName: s.leagueName,
|
|
tier: s.tier,
|
|
status: mapSponsorshipStatus(s.status),
|
|
startDate: start,
|
|
endDate: end,
|
|
price: s.pricing.amount,
|
|
impressions: s.metrics.impressions,
|
|
drivers: s.metrics.drivers,
|
|
};
|
|
});
|
|
}
|
|
|
|
function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
|
const router = useRouter();
|
|
|
|
const statusConfig = {
|
|
active: { icon: Check, color: 'text-performance-green', bg: 'bg-performance-green/10', label: 'Active' },
|
|
pending: { icon: Clock, color: 'text-warning-amber', bg: 'bg-warning-amber/10', label: 'Pending' },
|
|
expired: { icon: XCircle, color: 'text-gray-400', bg: 'bg-gray-400/10', label: 'Expired' },
|
|
};
|
|
|
|
const tierConfig = {
|
|
main: { color: 'text-primary-blue', bg: 'bg-primary-blue/10', border: 'border-primary-blue/30', label: 'Main Sponsor' },
|
|
secondary: { color: 'text-purple-400', bg: 'bg-purple-400/10', border: 'border-purple-400/30', label: 'Secondary' },
|
|
};
|
|
|
|
const status = statusConfig[sponsorship.status];
|
|
const tier = tierConfig[sponsorship.tier];
|
|
const StatusIcon = status.icon;
|
|
|
|
return (
|
|
<Card className="hover:border-charcoal-outline/80 transition-colors">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`px-2 py-1 rounded text-xs font-medium border ${tier.bg} ${tier.color} ${tier.border}`}>
|
|
{tier.label}
|
|
</div>
|
|
<div className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium ${status.bg} ${status.color}`}>
|
|
<StatusIcon className="w-3 h-3" />
|
|
{status.label}
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => router.push(`/leagues/${sponsorship.leagueId}`)}
|
|
className="text-xs"
|
|
>
|
|
<ExternalLink className="w-3 h-3 mr-1" />
|
|
View League
|
|
</Button>
|
|
</div>
|
|
|
|
<h3 className="text-lg font-semibold text-white mb-2">{sponsorship.leagueName}</h3>
|
|
|
|
<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="text-white font-semibold">{sponsorship.impressions.toLocaleString()}</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">
|
|
<Users className="w-3 h-3" />
|
|
Drivers
|
|
</div>
|
|
<div className="text-white font-semibold">{sponsorship.drivers}</div>
|
|
</div>
|
|
<div className="bg-iron-gray/50 rounded-lg p-3">
|
|
<div className="flex items-center gap-1 text-gray-400 text-xs mb-1">
|
|
<Calendar className="w-3 h-3" />
|
|
Period
|
|
</div>
|
|
<div className="text-white font-semibold text-xs">
|
|
{sponsorship.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - {sponsorship.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
|
</div>
|
|
</div>
|
|
<div className="bg-iron-gray/50 rounded-lg p-3">
|
|
<div className="flex items-center gap-1 text-gray-400 text-xs mb-1">
|
|
<Trophy className="w-3 h-3" />
|
|
Investment
|
|
</div>
|
|
<div className="text-white font-semibold">${sponsorship.price}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-3 border-t border-charcoal-outline/50">
|
|
<span className="text-xs text-gray-500">
|
|
{Math.ceil((sponsorship.endDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24))} days remaining
|
|
</span>
|
|
<Button
|
|
variant="secondary"
|
|
className="text-xs"
|
|
onClick={() => router.push(`/sponsor/campaigns/${sponsorship.id}`)}
|
|
>
|
|
View Details
|
|
<ChevronRight className="w-3 h-3 ml-1" />
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default function SponsorCampaignsPage() {
|
|
const router = useRouter();
|
|
const [filter, setFilter] = useState<'all' | 'active' | 'pending' | 'expired'>('all');
|
|
const [sponsorships, setSponsorships] = useState<Sponsorship[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
async function fetchSponsorships() {
|
|
try {
|
|
const response = await fetch('/api/sponsors/sponsorships');
|
|
if (!response.ok) {
|
|
if (!isMounted) return;
|
|
setSponsorships([]);
|
|
return;
|
|
}
|
|
const json: SponsorSponsorshipsResponse = await response.json();
|
|
if (!isMounted) return;
|
|
setSponsorships(mapApiToSponsorships(json));
|
|
} catch {
|
|
if (!isMounted) return;
|
|
setSponsorships([]);
|
|
} finally {
|
|
if (isMounted) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchSponsorships();
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
|
|
const filteredSponsorships = filter === 'all'
|
|
? sponsorships
|
|
: sponsorships.filter(s => s.status === filter);
|
|
|
|
const stats = {
|
|
total: sponsorships.length,
|
|
active: sponsorships.filter(s => s.status === 'active').length,
|
|
pending: sponsorships.filter(s => s.status === 'pending').length,
|
|
totalInvestment: sponsorships.reduce((sum, s) => sum + s.price, 0),
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="max-w-6xl mx-auto py-8 px-4">
|
|
<p className="text-gray-400">Loading sponsorships…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto py-8 px-4">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between 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 your league sponsorships</p>
|
|
</div>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => router.push('/leagues')}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Find Leagues to Sponsor
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-white">{stats.total}</div>
|
|
<div className="text-sm text-gray-400">Total Sponsorships</div>
|
|
</Card>
|
|
<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>
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-warning-amber">{stats.pending}</div>
|
|
<div className="text-sm text-gray-400">Pending</div>
|
|
</Card>
|
|
<Card className="p-4">
|
|
<div className="text-2xl font-bold text-white">${stats.totalInvestment.toLocaleString()}</div>
|
|
<div className="text-sm text-gray-400">Total Investment</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex items-center gap-2 mb-6">
|
|
{(['all', 'active', 'pending', 'expired'] as const).map((f) => (
|
|
<button
|
|
key={f}
|
|
onClick={() => setFilter(f)}
|
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
filter === f
|
|
? 'bg-primary-blue text-white'
|
|
: 'bg-iron-gray/50 text-gray-400 hover:bg-iron-gray'
|
|
}`}
|
|
>
|
|
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Sponsorship List */}
|
|
{filteredSponsorships.length === 0 ? (
|
|
<Card className="text-center py-12">
|
|
<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">Start sponsoring leagues to grow your brand visibility</p>
|
|
<Button variant="primary" onClick={() => router.push('/leagues')}>
|
|
Browse Leagues
|
|
</Button>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{filteredSponsorships.map((sponsorship) => (
|
|
<SponsorshipCard key={sponsorship.id} sponsorship={sponsorship} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Alpha Notice */}
|
|
<div className="mt-8 rounded-lg bg-warning-amber/10 border border-warning-amber/30 p-4">
|
|
<p className="text-xs text-gray-400">
|
|
<strong className="text-warning-amber">Alpha Note:</strong> Sponsorship data shown here is demonstration-only.
|
|
Real sponsorship management will be available when the system is fully implemented.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |