fix data flow issues
This commit is contained in:
@@ -2,44 +2,57 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import OnboardingWizard from '@/components/onboarding/OnboardingWizard';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
// TODO: Re-enable API integration once backend is ready
|
||||
// import { redirect } from 'next/navigation';
|
||||
|
||||
import OnboardingWizard from '@/components/onboarding/OnboardingWizard';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const router = useRouter();
|
||||
const { session } = useAuth();
|
||||
const { driverService } = useServices();
|
||||
const [checking, setChecking] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Re-enable auth check once backend is ready
|
||||
// For now, just show onboarding after a brief check
|
||||
const checkDemoMode = () => {
|
||||
// Check if user has demo mode cookie
|
||||
const cookies = document.cookie.split(';');
|
||||
const demoModeCookie = cookies.find(c => c.trim().startsWith('gridpilot_demo_mode='));
|
||||
|
||||
if (!demoModeCookie) {
|
||||
// Not logged in, redirect to auth
|
||||
router.push('/auth/login?returnTo=/onboarding');
|
||||
return;
|
||||
// If user is not authenticated, redirect to login
|
||||
if (!session) {
|
||||
router.replace('/auth/login?returnTo=/onboarding');
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const checkOnboarding = async () => {
|
||||
try {
|
||||
const driver = await driverService.getCurrentDriver();
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
// If driver profile exists, onboarding is complete – go to dashboard
|
||||
if (driver) {
|
||||
router.replace('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise allow onboarding wizard to render
|
||||
setChecking(false);
|
||||
} catch {
|
||||
// On error, allow onboarding to proceed so user isn't blocked
|
||||
if (!cancelled) {
|
||||
setChecking(false);
|
||||
}
|
||||
}
|
||||
|
||||
// For demo, skip onboarding and go to dashboard
|
||||
// In production, this would check if onboarding is complete
|
||||
router.push('/dashboard');
|
||||
};
|
||||
|
||||
// Brief delay to prevent flash
|
||||
const timer = setTimeout(() => {
|
||||
checkDemoMode();
|
||||
}, 500);
|
||||
checkOnboarding();
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [router]);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [session, driverService, router]);
|
||||
|
||||
// Show loading while checking
|
||||
// Show loading while checking auth/onboarding status
|
||||
if (checking) {
|
||||
return (
|
||||
<main className="min-h-screen bg-deep-graphite flex items-center justify-center">
|
||||
|
||||
@@ -14,19 +14,39 @@ import SimPlatformMockup from '@/components/mockups/SimPlatformMockup';
|
||||
import MockupStack from '@/components/ui/MockupStack';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { LandingService } from '@/lib/services/landing/LandingService';
|
||||
import { SessionService } from '@/lib/services/auth/SessionService';
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
export default async function HomePage() {
|
||||
const session = null; // TODO
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
|
||||
const authApiClient = new AuthApiClient(baseUrl, errorReporter, logger);
|
||||
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
const sessionService = new SessionService(authApiClient);
|
||||
const landingService = new LandingService(racesApiClient, leaguesApiClient, teamsApiClient);
|
||||
|
||||
const session = await sessionService.getSession();
|
||||
if (session) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
const mode = getAppMode();
|
||||
const isAlpha = mode === 'alpha';
|
||||
// const upcomingRaces = getUpcomingRaces(3);
|
||||
const upcomingRaces = []; // TODO
|
||||
const topLeagues = []; // TODO
|
||||
const teams = []; // TODO
|
||||
const discovery = await landingService.getHomeDiscovery();
|
||||
const upcomingRaces = discovery.upcomingRaces;
|
||||
const topLeagues = discovery.topLeagues;
|
||||
const teams = discovery.teams;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
@@ -332,10 +352,7 @@ export default async function HomePage() {
|
||||
<p className="text-xs text-gray-400 truncate">{race.car}</p>
|
||||
</div>
|
||||
<div className="text-right text-xs text-gray-500 whitespace-nowrap">
|
||||
{race.scheduledAt.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
{race.formattedDate}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
@@ -10,6 +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 { SponsorService } from '@/lib/services/sponsors/SponsorService';
|
||||
import { ServiceFactory } from '@/lib/services/ServiceFactory';
|
||||
import {
|
||||
CreditCard,
|
||||
DollarSign,
|
||||
@@ -73,108 +76,17 @@ interface BillingStats {
|
||||
// Mock Data
|
||||
// ============================================================================
|
||||
|
||||
const MOCK_PAYMENT_METHODS: PaymentMethod[] = [
|
||||
{
|
||||
id: 'pm-1',
|
||||
type: 'card',
|
||||
last4: '4242',
|
||||
brand: 'Visa',
|
||||
isDefault: true,
|
||||
expiryMonth: 12,
|
||||
expiryYear: 2027,
|
||||
},
|
||||
{
|
||||
id: 'pm-2',
|
||||
type: 'card',
|
||||
last4: '5555',
|
||||
brand: 'Mastercard',
|
||||
isDefault: false,
|
||||
expiryMonth: 6,
|
||||
expiryYear: 2026,
|
||||
},
|
||||
{
|
||||
id: 'pm-3',
|
||||
type: 'sepa',
|
||||
last4: '8901',
|
||||
bankName: 'Deutsche Bank',
|
||||
isDefault: false,
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_INVOICES: Invoice[] = [
|
||||
{
|
||||
id: 'inv-1',
|
||||
invoiceNumber: 'GP-2025-001234',
|
||||
date: new Date('2025-11-01'),
|
||||
dueDate: new Date('2025-11-15'),
|
||||
amount: 1090.91,
|
||||
vatAmount: 207.27,
|
||||
totalAmount: 1298.18,
|
||||
status: 'paid',
|
||||
description: 'GT3 Pro Championship - Primary Sponsor (Q4 2025)',
|
||||
sponsorshipType: 'league',
|
||||
pdfUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 'inv-2',
|
||||
invoiceNumber: 'GP-2025-001235',
|
||||
date: new Date('2025-10-01'),
|
||||
dueDate: new Date('2025-10-15'),
|
||||
amount: 363.64,
|
||||
vatAmount: 69.09,
|
||||
totalAmount: 432.73,
|
||||
status: 'paid',
|
||||
description: 'Team Velocity - Gear Sponsor (Q4 2025)',
|
||||
sponsorshipType: 'team',
|
||||
pdfUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 'inv-3',
|
||||
invoiceNumber: 'GP-2025-001236',
|
||||
date: new Date('2025-12-01'),
|
||||
dueDate: new Date('2025-12-15'),
|
||||
amount: 318.18,
|
||||
vatAmount: 60.45,
|
||||
totalAmount: 378.63,
|
||||
status: 'pending',
|
||||
description: 'Alex Thompson - Driver Sponsorship (Dec 2025)',
|
||||
sponsorshipType: 'driver',
|
||||
pdfUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 'inv-4',
|
||||
invoiceNumber: 'GP-2025-001237',
|
||||
date: new Date('2025-11-15'),
|
||||
dueDate: new Date('2025-11-29'),
|
||||
amount: 454.55,
|
||||
vatAmount: 86.36,
|
||||
totalAmount: 540.91,
|
||||
status: 'overdue',
|
||||
description: 'Touring Car Cup - Secondary Sponsor (Q1 2026)',
|
||||
sponsorshipType: 'league',
|
||||
pdfUrl: '#',
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_STATS: BillingStats = {
|
||||
totalSpent: 12450,
|
||||
pendingAmount: 919.54,
|
||||
nextPaymentDate: new Date('2025-12-15'),
|
||||
nextPaymentAmount: 378.63,
|
||||
activeSponsorships: 6,
|
||||
averageMonthlySpend: 2075,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Components
|
||||
// ============================================================================
|
||||
|
||||
function PaymentMethodCard({
|
||||
method,
|
||||
method,
|
||||
onSetDefault,
|
||||
onRemove
|
||||
}: {
|
||||
method: PaymentMethod;
|
||||
onRemove
|
||||
}: {
|
||||
method: any;
|
||||
onSetDefault: () => void;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
@@ -214,16 +126,16 @@ function PaymentMethodCard({
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white">{getLabel()}</span>
|
||||
<span className="font-medium text-white">{method.displayLabel}</span>
|
||||
{method.isDefault && (
|
||||
<span className="px-2 py-0.5 rounded-full text-xs bg-primary-blue/20 text-primary-blue font-medium">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{method.expiryMonth && method.expiryYear && (
|
||||
{method.expiryDisplay && (
|
||||
<span className="text-sm text-gray-500">
|
||||
Expires {String(method.expiryMonth).padStart(2, '0')}/{method.expiryYear}
|
||||
Expires {method.expiryDisplay}
|
||||
</span>
|
||||
)}
|
||||
{method.type === 'sepa' && (
|
||||
@@ -246,7 +158,7 @@ function PaymentMethodCard({
|
||||
);
|
||||
}
|
||||
|
||||
function InvoiceRow({ invoice, index }: { invoice: Invoice; index: number }) {
|
||||
function InvoiceRow({ invoice, index }: { invoice: any; index: number }) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const statusConfig = {
|
||||
@@ -313,7 +225,7 @@ function InvoiceRow({ invoice, index }: { invoice: Invoice; index: number }) {
|
||||
<span>{invoice.invoiceNumber}</span>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{invoice.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||
{invoice.formattedDate}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,13 +234,13 @@ function InvoiceRow({ invoice, index }: { invoice: Invoice; index: number }) {
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-white">
|
||||
€{invoice.totalAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}
|
||||
{invoice.formattedTotalAmount}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
incl. €{invoice.vatAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })} VAT
|
||||
incl. {invoice.formattedVatAmount} VAT
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${status.bg} ${status.color} border ${status.border}`}>
|
||||
<StatusIcon className="w-3 h-3" />
|
||||
{status.label}
|
||||
@@ -349,9 +261,49 @@ function InvoiceRow({ invoice, index }: { invoice: Invoice; index: number }) {
|
||||
|
||||
export default function SponsorBillingPage() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [paymentMethods, setPaymentMethods] = useState(MOCK_PAYMENT_METHODS);
|
||||
const [data, setData] = useState<BillingViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showAllInvoices, setShowAllInvoices] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadBilling = async () => {
|
||||
try {
|
||||
const sponsorService = ServiceFactory.getSponsorService();
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
loadBilling();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-5xl 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 billing data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSetDefault = (methodId: string) => {
|
||||
setPaymentMethods(methods =>
|
||||
methods.map(m => ({ ...m, isDefault: m.id === methodId }))
|
||||
@@ -364,7 +316,17 @@ export default function SponsorBillingPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const displayedInvoices = showAllInvoices ? MOCK_INVOICES : MOCK_INVOICES.slice(0, 4);
|
||||
const handleSetDefault = (methodId: string) => {
|
||||
// In a real app, this would call an API
|
||||
console.log('Setting default payment method:', methodId);
|
||||
};
|
||||
|
||||
const handleRemoveMethod = (methodId: string) => {
|
||||
if (confirm('Remove this payment method?')) {
|
||||
// In a real app, this would call an API
|
||||
console.log('Removing payment method:', methodId);
|
||||
}
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
@@ -404,7 +366,7 @@ export default function SponsorBillingPage() {
|
||||
<StatCard
|
||||
icon={DollarSign}
|
||||
label="Total Spent"
|
||||
value={`€${MOCK_STATS.totalSpent.toLocaleString('de-DE')}`}
|
||||
value={data.stats.formattedTotalSpent}
|
||||
subValue="All time"
|
||||
color="text-performance-green"
|
||||
bgColor="bg-performance-green/10"
|
||||
@@ -412,23 +374,23 @@ export default function SponsorBillingPage() {
|
||||
<StatCard
|
||||
icon={AlertTriangle}
|
||||
label="Pending Payments"
|
||||
value={`€${MOCK_STATS.pendingAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`}
|
||||
subValue={`${MOCK_INVOICES.filter(i => i.status === 'pending' || i.status === 'overdue').length} invoices`}
|
||||
value={data.stats.formattedPendingAmount}
|
||||
subValue={`${data.invoices.filter(i => i.status === 'pending' || i.status === 'overdue').length} invoices`}
|
||||
color="text-warning-amber"
|
||||
bgColor="bg-warning-amber/10"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Calendar}
|
||||
label="Next Payment"
|
||||
value={MOCK_STATS.nextPaymentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
subValue={`€${MOCK_STATS.nextPaymentAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`}
|
||||
value={data.stats.formattedNextPaymentDate}
|
||||
subValue={data.stats.formattedNextPaymentAmount}
|
||||
color="text-primary-blue"
|
||||
bgColor="bg-primary-blue/10"
|
||||
/>
|
||||
<StatCard
|
||||
icon={TrendingUp}
|
||||
label="Monthly Average"
|
||||
value={`€${MOCK_STATS.averageMonthlySpend.toLocaleString('de-DE')}`}
|
||||
value={data.stats.formattedAverageMonthlySpend}
|
||||
subValue="Last 6 months"
|
||||
color="text-gray-400"
|
||||
bgColor="bg-iron-gray"
|
||||
@@ -449,10 +411,10 @@ export default function SponsorBillingPage() {
|
||||
}
|
||||
/>
|
||||
<div className="p-5 space-y-3">
|
||||
{paymentMethods.map((method) => (
|
||||
<PaymentMethodCard
|
||||
key={method.id}
|
||||
method={method}
|
||||
{data.paymentMethods.map((method) => (
|
||||
<PaymentMethodCard
|
||||
key={method.id}
|
||||
method={method}
|
||||
onSetDefault={() => handleSetDefault(method.id)}
|
||||
onRemove={() => handleRemoveMethod(method.id)}
|
||||
/>
|
||||
@@ -482,18 +444,18 @@ export default function SponsorBillingPage() {
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
{displayedInvoices.map((invoice, index) => (
|
||||
{data.invoices.slice(0, showAllInvoices ? data.invoices.length : 4).map((invoice, index) => (
|
||||
<InvoiceRow key={invoice.id} invoice={invoice} index={index} />
|
||||
))}
|
||||
</div>
|
||||
{MOCK_INVOICES.length > 4 && (
|
||||
{data.invoices.length > 4 && (
|
||||
<div className="p-4 border-t border-charcoal-outline">
|
||||
<Button
|
||||
variant="secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => setShowAllInvoices(!showAllInvoices)}
|
||||
>
|
||||
{showAllInvoices ? 'Show Less' : `View All ${MOCK_INVOICES.length} Invoices`}
|
||||
{showAllInvoices ? 'Show Less' : `View All ${data.invoices.length} Invoices`}
|
||||
<ChevronRight className={`w-4 h-4 ml-2 transition-transform ${showAllInvoices ? 'rotate-90' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ 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 { SponsorService } from '@/lib/services/sponsors/SponsorService';
|
||||
import { ServiceFactory } from '@/lib/services/ServiceFactory';
|
||||
import { SponsorSponsorshipsViewModel } from '@/lib/view-models/SponsorSponsorshipsViewModel';
|
||||
import {
|
||||
Megaphone,
|
||||
Trophy,
|
||||
@@ -69,241 +72,6 @@ interface Sponsorship {
|
||||
// Mock Data - Updated to show application workflow
|
||||
// ============================================================================
|
||||
|
||||
const MOCK_SPONSORSHIPS: Sponsorship[] = [
|
||||
// Active sponsorships (approved and started)
|
||||
{
|
||||
id: 's1',
|
||||
type: 'leagues',
|
||||
entityId: 'l1',
|
||||
entityName: 'GT3 Masters Championship',
|
||||
tier: 'main',
|
||||
status: 'active',
|
||||
applicationDate: new Date('2025-09-15'),
|
||||
approvalDate: new Date('2025-09-18'),
|
||||
startDate: new Date('2025-10-01'),
|
||||
endDate: new Date('2026-02-28'),
|
||||
price: 1200,
|
||||
impressions: 45200,
|
||||
impressionsChange: 12.5,
|
||||
engagement: 4.2,
|
||||
details: '48 drivers • 12 races',
|
||||
entityOwner: 'Championship Admin',
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
type: 'leagues',
|
||||
entityId: 'l2',
|
||||
entityName: 'Endurance Pro Series',
|
||||
tier: 'secondary',
|
||||
status: 'active',
|
||||
applicationDate: new Date('2025-10-20'),
|
||||
approvalDate: new Date('2025-10-25'),
|
||||
startDate: new Date('2025-11-01'),
|
||||
endDate: new Date('2026-03-31'),
|
||||
price: 500,
|
||||
impressions: 38400,
|
||||
impressionsChange: 8.3,
|
||||
engagement: 3.8,
|
||||
details: '72 drivers • 6 races',
|
||||
entityOwner: 'Endurance Racing LLC',
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
type: 'teams',
|
||||
entityId: 't1',
|
||||
entityName: 'Velocity Racing',
|
||||
status: 'active',
|
||||
applicationDate: new Date('2025-08-25'),
|
||||
approvalDate: new Date('2025-08-26'),
|
||||
startDate: new Date('2025-09-01'),
|
||||
endDate: new Date('2026-08-31'),
|
||||
price: 400,
|
||||
impressions: 12300,
|
||||
impressionsChange: 5.2,
|
||||
engagement: 5.1,
|
||||
details: '4 drivers • GT3 & LMP',
|
||||
entityOwner: 'Team Principal',
|
||||
},
|
||||
// Pending approval (waiting for entity owner)
|
||||
{
|
||||
id: 's9',
|
||||
type: 'leagues',
|
||||
entityId: 'l3',
|
||||
entityName: 'Formula Sim Series',
|
||||
tier: 'main',
|
||||
status: 'pending_approval',
|
||||
applicationDate: new Date('2025-12-14'),
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-06-30'),
|
||||
price: 1500,
|
||||
impressions: 0,
|
||||
details: '36 drivers • F3 class',
|
||||
entityOwner: 'Formula Sim Organization',
|
||||
applicationMessage: 'We would love to be your main sponsor for the upcoming season.',
|
||||
},
|
||||
{
|
||||
id: 's10',
|
||||
type: 'teams',
|
||||
entityId: 't3',
|
||||
entityName: 'Phoenix Racing Team',
|
||||
status: 'pending_approval',
|
||||
applicationDate: new Date('2025-12-12'),
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-12-31'),
|
||||
price: 600,
|
||||
impressions: 0,
|
||||
details: '5 drivers • Multi-class',
|
||||
entityOwner: 'Phoenix Team Manager',
|
||||
applicationMessage: 'Interested in sponsoring your team for the full 2026 season.',
|
||||
},
|
||||
{
|
||||
id: 's11',
|
||||
type: 'drivers',
|
||||
entityId: 'd3',
|
||||
entityName: 'James Rodriguez',
|
||||
status: 'pending_approval',
|
||||
applicationDate: new Date('2025-12-10'),
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-12-31'),
|
||||
price: 250,
|
||||
impressions: 0,
|
||||
details: 'Rising rookie • GT3 Masters',
|
||||
entityOwner: 'James Rodriguez',
|
||||
applicationMessage: 'Would like to support your racing career.',
|
||||
},
|
||||
// Recently approved (not yet started)
|
||||
{
|
||||
id: 's12',
|
||||
type: 'races',
|
||||
entityId: 'r1',
|
||||
entityName: 'Spa 24 Hours',
|
||||
status: 'approved',
|
||||
applicationDate: new Date('2025-12-01'),
|
||||
approvalDate: new Date('2025-12-05'),
|
||||
startDate: new Date('2025-12-20'),
|
||||
endDate: new Date('2025-12-21'),
|
||||
price: 300,
|
||||
impressions: 0,
|
||||
details: 'Endurance Pro Series • Dec 20-21',
|
||||
entityOwner: 'Race Director',
|
||||
},
|
||||
{
|
||||
id: 's13',
|
||||
type: 'drivers',
|
||||
entityId: 'd4',
|
||||
entityName: 'Emma Wilson',
|
||||
status: 'approved',
|
||||
applicationDate: new Date('2025-12-08'),
|
||||
approvalDate: new Date('2025-12-10'),
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-12-31'),
|
||||
price: 180,
|
||||
impressions: 0,
|
||||
details: 'Touring Car specialist',
|
||||
entityOwner: 'Emma Wilson',
|
||||
},
|
||||
// Rejected applications
|
||||
{
|
||||
id: 's14',
|
||||
type: 'leagues',
|
||||
entityId: 'l4',
|
||||
entityName: 'Elite GT Championship',
|
||||
tier: 'main',
|
||||
status: 'rejected',
|
||||
applicationDate: new Date('2025-11-20'),
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-06-30'),
|
||||
price: 2000,
|
||||
impressions: 0,
|
||||
details: '24 drivers • Invite-only',
|
||||
entityOwner: 'Elite Racing Committee',
|
||||
rejectionReason: 'Main sponsor position already filled for the upcoming season.',
|
||||
},
|
||||
{
|
||||
id: 's15',
|
||||
type: 'teams',
|
||||
entityId: 't4',
|
||||
entityName: 'Apex Motorsport',
|
||||
status: 'rejected',
|
||||
applicationDate: new Date('2025-11-15'),
|
||||
startDate: new Date('2026-01-01'),
|
||||
endDate: new Date('2026-12-31'),
|
||||
price: 450,
|
||||
impressions: 0,
|
||||
details: '3 drivers • LMP2',
|
||||
entityOwner: 'Apex Team Owner',
|
||||
rejectionReason: 'Already have exclusive sponsor agreement in this category.',
|
||||
},
|
||||
// Existing active ones
|
||||
{
|
||||
id: 's4',
|
||||
type: 'teams',
|
||||
entityId: 't2',
|
||||
entityName: 'Storm Motorsport',
|
||||
status: 'active',
|
||||
applicationDate: new Date('2025-10-01'),
|
||||
approvalDate: new Date('2025-10-05'),
|
||||
startDate: new Date('2025-10-15'),
|
||||
endDate: new Date('2026-10-14'),
|
||||
price: 350,
|
||||
impressions: 8900,
|
||||
impressionsChange: -2.1,
|
||||
engagement: 4.5,
|
||||
details: '3 drivers • Formula',
|
||||
entityOwner: 'Storm Racing LLC',
|
||||
},
|
||||
{
|
||||
id: 's5',
|
||||
type: 'drivers',
|
||||
entityId: 'd1',
|
||||
entityName: 'Max Velocity',
|
||||
status: 'active',
|
||||
applicationDate: new Date('2025-10-20'),
|
||||
approvalDate: new Date('2025-10-21'),
|
||||
startDate: new Date('2025-11-01'),
|
||||
endDate: new Date('2026-10-31'),
|
||||
price: 200,
|
||||
impressions: 8200,
|
||||
impressionsChange: 15.8,
|
||||
engagement: 6.2,
|
||||
details: 'Velocity Racing • P1 in GT3 Masters',
|
||||
entityOwner: 'Max Velocity',
|
||||
},
|
||||
{
|
||||
id: 's6',
|
||||
type: 'drivers',
|
||||
entityId: 'd2',
|
||||
entityName: 'Sarah Storm',
|
||||
status: 'active',
|
||||
applicationDate: new Date('2025-09-25'),
|
||||
approvalDate: new Date('2025-09-26'),
|
||||
startDate: new Date('2025-10-01'),
|
||||
endDate: new Date('2026-09-30'),
|
||||
price: 150,
|
||||
impressions: 6100,
|
||||
impressionsChange: 22.4,
|
||||
engagement: 5.8,
|
||||
details: 'Storm Motorsport • Rising star',
|
||||
entityOwner: 'Sarah Storm',
|
||||
},
|
||||
{
|
||||
id: 's8',
|
||||
type: 'platform',
|
||||
entityId: 'p1',
|
||||
entityName: 'Homepage Banner',
|
||||
status: 'active',
|
||||
applicationDate: new Date('2025-11-25'),
|
||||
approvalDate: new Date('2025-11-25'),
|
||||
startDate: new Date('2025-12-01'),
|
||||
endDate: new Date('2025-12-31'),
|
||||
price: 500,
|
||||
impressions: 52000,
|
||||
impressionsChange: 3.4,
|
||||
engagement: 2.1,
|
||||
details: 'Header position • All pages',
|
||||
entityOwner: 'GridPilot',
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
@@ -360,7 +128,7 @@ const STATUS_CONFIG = {
|
||||
// Components
|
||||
// ============================================================================
|
||||
|
||||
function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
||||
function SponsorshipCard({ sponsorship }: { sponsorship: any }) {
|
||||
const router = useRouter();
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
@@ -492,7 +260,7 @@ function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
||||
Impressions
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white font-semibold">{sponsorship.impressions.toLocaleString()}</span>
|
||||
<span className="text-white font-semibold">{sponsorship.formattedImpressions}</span>
|
||||
{sponsorship.impressionsChange !== undefined && sponsorship.impressionsChange !== 0 && (
|
||||
<span className={`text-xs flex items-center ${
|
||||
sponsorship.impressionsChange > 0 ? 'text-performance-green' : 'text-racing-red'
|
||||
@@ -529,7 +297,7 @@ function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
||||
<Trophy className="w-3 h-3" />
|
||||
Investment
|
||||
</div>
|
||||
<div className="text-white font-semibold">${sponsorship.price}</div>
|
||||
<div className="text-white font-semibold">{sponsorship.formattedPrice}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -539,11 +307,11 @@ function SponsorshipCard({ sponsorship }: { sponsorship: Sponsorship }) {
|
||||
<div className="flex items-center gap-4 mb-4 text-sm">
|
||||
<div className="flex items-center gap-1 text-gray-400">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
{sponsorship.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - {sponsorship.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
||||
{sponsorship.periodDisplay}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-gray-400">
|
||||
<Trophy className="w-3.5 h-3.5" />
|
||||
${sponsorship.price}
|
||||
{sponsorship.formattedPrice}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -597,20 +365,59 @@ export default function SponsorCampaignsPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
|
||||
const initialType = (searchParams.get('type') as SponsorshipType) || 'all';
|
||||
const [typeFilter, setTypeFilter] = useState<SponsorshipType>(initialType);
|
||||
const [statusFilter, setStatusFilter] = useState<SponsorshipStatus>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [data, setData] = useState<SponsorSponsorshipsViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setLoading(false), 300);
|
||||
return () => clearTimeout(timer);
|
||||
const loadSponsorships = async () => {
|
||||
try {
|
||||
const sponsorService = ServiceFactory.getSponsorService();
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
loadSponsorships();
|
||||
}, []);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 sponsorships data available'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Filter sponsorships
|
||||
const filteredSponsorships = MOCK_SPONSORSHIPS.filter(s => {
|
||||
const filteredSponsorships = data.sponsorships.filter(s => {
|
||||
if (typeFilter !== 'all' && s.type !== typeFilter) return false;
|
||||
if (statusFilter !== 'all' && s.status !== statusFilter) return false;
|
||||
if (searchQuery && !s.entityName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||
@@ -619,22 +426,22 @@ export default function SponsorCampaignsPage() {
|
||||
|
||||
// Calculate stats
|
||||
const stats = {
|
||||
total: MOCK_SPONSORSHIPS.length,
|
||||
active: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').length,
|
||||
pending: MOCK_SPONSORSHIPS.filter(s => s.status === 'pending_approval').length,
|
||||
approved: MOCK_SPONSORSHIPS.filter(s => s.status === 'approved').length,
|
||||
rejected: MOCK_SPONSORSHIPS.filter(s => s.status === 'rejected').length,
|
||||
totalInvestment: MOCK_SPONSORSHIPS.filter(s => s.status === 'active').reduce((sum, s) => sum + s.price, 0),
|
||||
totalImpressions: MOCK_SPONSORSHIPS.reduce((sum, s) => sum + s.impressions, 0),
|
||||
total: data.sponsorships.length,
|
||||
active: data.sponsorships.filter(s => s.status === 'active').length,
|
||||
pending: data.sponsorships.filter(s => s.status === 'pending_approval').length,
|
||||
approved: data.sponsorships.filter(s => s.status === 'approved').length,
|
||||
rejected: data.sponsorships.filter(s => s.status === 'rejected').length,
|
||||
totalInvestment: data.sponsorships.filter(s => s.status === 'active').reduce((sum, s) => sum + s.price, 0),
|
||||
totalImpressions: data.sponsorships.reduce((sum, s) => sum + s.impressions, 0),
|
||||
};
|
||||
|
||||
// Stats by type
|
||||
const statsByType = {
|
||||
leagues: MOCK_SPONSORSHIPS.filter(s => s.type === 'leagues').length,
|
||||
teams: MOCK_SPONSORSHIPS.filter(s => s.type === 'teams').length,
|
||||
drivers: MOCK_SPONSORSHIPS.filter(s => s.type === 'drivers').length,
|
||||
races: MOCK_SPONSORSHIPS.filter(s => s.type === 'races').length,
|
||||
platform: MOCK_SPONSORSHIPS.filter(s => s.type === 'platform').length,
|
||||
leagues: data.sponsorships.filter(s => s.type === 'leagues').length,
|
||||
teams: data.sponsorships.filter(s => s.type === 'teams').length,
|
||||
drivers: data.sponsorships.filter(s => s.type === 'drivers').length,
|
||||
races: data.sponsorships.filter(s => s.type === 'races').length,
|
||||
platform: data.sponsorships.filter(s => s.type === 'platform').length,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -796,9 +603,9 @@ export default function SponsorCampaignsPage() {
|
||||
const config = status === 'all'
|
||||
? { label: 'All', color: 'text-gray-400' }
|
||||
: STATUS_CONFIG[status];
|
||||
const count = status === 'all'
|
||||
? stats.total
|
||||
: MOCK_SPONSORSHIPS.filter(s => s.status === status).length;
|
||||
const count = status === 'all'
|
||||
? stats.total
|
||||
: data.sponsorships.filter(s => s.status === status).length;
|
||||
return (
|
||||
<button
|
||||
key={status}
|
||||
|
||||
@@ -31,52 +31,9 @@ import {
|
||||
RefreshCw
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
// Static mock data for prototype
|
||||
const MOCK_SPONSOR_DATA = {
|
||||
sponsorId: 'demo-sponsor-1',
|
||||
sponsorName: 'Acme Racing Co.',
|
||||
metrics: {
|
||||
totalImpressions: 127450,
|
||||
impressionsChange: 12.5,
|
||||
uniqueViewers: 34200,
|
||||
viewersChange: 8.3,
|
||||
activeSponsors: 7,
|
||||
totalInvestment: 4850,
|
||||
avgEngagement: 4.2,
|
||||
engagementChange: 0.8,
|
||||
},
|
||||
sponsorships: {
|
||||
leagues: [
|
||||
{ id: 'l1', name: 'GT3 Masters Championship', tier: 'main', drivers: 48, impressions: 45200, status: 'active' },
|
||||
{ id: 'l2', name: 'Endurance Pro Series', tier: 'secondary', drivers: 72, impressions: 38400, status: 'active' },
|
||||
],
|
||||
teams: [
|
||||
{ id: 't1', name: 'Velocity Racing', drivers: 4, impressions: 12300, status: 'active' },
|
||||
{ id: 't2', name: 'Storm Motorsport', drivers: 3, impressions: 8900, status: 'active' },
|
||||
],
|
||||
drivers: [
|
||||
{ id: 'd1', name: 'Max Velocity', team: 'Velocity Racing', impressions: 8200, status: 'active' },
|
||||
{ id: 'd2', name: 'Sarah Storm', team: 'Storm Motorsport', impressions: 6100, status: 'active' },
|
||||
],
|
||||
races: [
|
||||
{ id: 'r1', name: 'Spa 24 Hours', league: 'Endurance Pro', impressions: 15600, date: '2025-12-20', status: 'upcoming' },
|
||||
],
|
||||
platform: [
|
||||
{ id: 'p1', name: 'Homepage Banner', placement: 'Header', impressions: 52000, status: 'active' },
|
||||
],
|
||||
},
|
||||
recentActivity: [
|
||||
{ id: 'a1', type: 'race', message: 'GT3 Masters Championship race completed', time: '2 hours ago', impressions: 1240 },
|
||||
{ id: 'a2', type: 'driver', message: 'Max Velocity finished P1 at Monza', time: '5 hours ago', impressions: 890 },
|
||||
{ id: 'a3', type: 'league', message: 'New driver joined Endurance Pro Series', time: '1 day ago', impressions: null },
|
||||
{ id: 'a4', type: 'team', message: 'Velocity Racing won team championship', time: '2 days ago', impressions: 2100 },
|
||||
],
|
||||
upcomingRenewals: [
|
||||
{ id: 'ren1', name: 'GT3 Masters Championship', type: 'league', renewDate: '2026-01-15', price: 1200 },
|
||||
{ id: 'ren2', name: 'Homepage Banner', type: 'platform', renewDate: '2025-12-31', price: 500 },
|
||||
],
|
||||
};
|
||||
import { SponsorService } from '@/lib/services/sponsors/SponsorService';
|
||||
import { SponsorDashboardViewModel } from '@/lib/view-models/SponsorDashboardViewModel';
|
||||
import { ServiceFactory } from '@/lib/services/ServiceFactory';
|
||||
|
||||
// Metric Card Component
|
||||
function MetricCard({
|
||||
@@ -169,26 +126,18 @@ function SponsorshipCategoryCard({
|
||||
}
|
||||
|
||||
// Activity Item
|
||||
function ActivityItem({ activity }: { activity: typeof MOCK_SPONSOR_DATA.recentActivity[0] }) {
|
||||
const typeColors = {
|
||||
race: 'bg-warning-amber',
|
||||
league: 'bg-primary-blue',
|
||||
team: 'bg-purple-400',
|
||||
driver: 'bg-performance-green',
|
||||
platform: 'bg-racing-red',
|
||||
};
|
||||
|
||||
function ActivityItem({ activity }: { activity: any }) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 py-3 border-b border-charcoal-outline/50 last:border-b-0">
|
||||
<div className={`w-2 h-2 rounded-full mt-2 ${typeColors[activity.type as keyof typeof typeColors] || 'bg-gray-500'}`} />
|
||||
<div className={`w-2 h-2 rounded-full mt-2 ${activity.typeColor}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-white truncate">{activity.message}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-gray-500">{activity.time}</span>
|
||||
{activity.impressions && (
|
||||
{activity.formattedImpressions && (
|
||||
<>
|
||||
<span className="text-xs text-gray-600">•</span>
|
||||
<span className="text-xs text-gray-400">{activity.impressions.toLocaleString()} views</span>
|
||||
<span className="text-xs text-gray-400">{activity.formattedImpressions} views</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -198,7 +147,7 @@ function ActivityItem({ activity }: { activity: typeof MOCK_SPONSOR_DATA.recentA
|
||||
}
|
||||
|
||||
// Renewal Alert
|
||||
function RenewalAlert({ renewal }: { renewal: typeof MOCK_SPONSOR_DATA.upcomingRenewals[0] }) {
|
||||
function RenewalAlert({ renewal }: { renewal: any }) {
|
||||
const typeIcons = {
|
||||
league: Trophy,
|
||||
team: Users,
|
||||
@@ -206,7 +155,7 @@ function RenewalAlert({ renewal }: { renewal: typeof MOCK_SPONSOR_DATA.upcomingR
|
||||
race: Flag,
|
||||
platform: Megaphone,
|
||||
};
|
||||
const Icon = typeIcons[renewal.type as keyof typeof typeIcons] || Trophy;
|
||||
const Icon = typeIcons[renewal.type] || Trophy;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-warning-amber/10 border border-warning-amber/30">
|
||||
@@ -214,11 +163,11 @@ function RenewalAlert({ renewal }: { renewal: typeof MOCK_SPONSOR_DATA.upcomingR
|
||||
<Icon className="w-4 h-4 text-warning-amber" />
|
||||
<div>
|
||||
<p className="text-sm text-white">{renewal.name}</p>
|
||||
<p className="text-xs text-gray-400">Renews {renewal.renewDate}</p>
|
||||
<p className="text-xs text-gray-400">Renews {renewal.formattedRenewDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-semibold text-white">${renewal.price}</p>
|
||||
<p className="text-sm font-semibold text-white">{renewal.formattedPrice}</p>
|
||||
<Button variant="secondary" className="text-xs mt-1 py-1 px-2 min-h-0">
|
||||
Renew
|
||||
</Button>
|
||||
@@ -231,38 +180,52 @@ export default function SponsorDashboardPage() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
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);
|
||||
|
||||
// Simulate loading
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setLoading(false), 500);
|
||||
return () => clearTimeout(timer);
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const sponsorService = ServiceFactory.getSponsorService();
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
loadDashboard();
|
||||
}, []);
|
||||
|
||||
const data = MOCK_SPONSOR_DATA;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate category totals
|
||||
const categoryData = {
|
||||
leagues: {
|
||||
count: data.sponsorships.leagues.length,
|
||||
impressions: data.sponsorships.leagues.reduce((sum, l) => sum + l.impressions, 0),
|
||||
},
|
||||
teams: {
|
||||
count: data.sponsorships.teams.length,
|
||||
impressions: data.sponsorships.teams.reduce((sum, t) => sum + t.impressions, 0),
|
||||
},
|
||||
drivers: {
|
||||
count: data.sponsorships.drivers.length,
|
||||
impressions: data.sponsorships.drivers.reduce((sum, d) => sum + d.impressions, 0),
|
||||
},
|
||||
races: {
|
||||
count: data.sponsorships.races.length,
|
||||
impressions: data.sponsorships.races.reduce((sum, r) => sum + r.impressions, 0),
|
||||
},
|
||||
platform: {
|
||||
count: data.sponsorships.platform.length,
|
||||
impressions: data.sponsorships.platform.reduce((sum, p) => sum + p.impressions, 0),
|
||||
},
|
||||
};
|
||||
if (error || !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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categoryData = data.categoryData;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -317,7 +280,7 @@ export default function SponsorDashboardPage() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<MetricCard
|
||||
title="Total Impressions"
|
||||
value={data.metrics.totalImpressions}
|
||||
value={data.totalImpressions}
|
||||
change={data.metrics.impressionsChange}
|
||||
icon={Eye}
|
||||
delay={0}
|
||||
@@ -331,15 +294,15 @@ export default function SponsorDashboardPage() {
|
||||
/>
|
||||
<MetricCard
|
||||
title="Engagement Rate"
|
||||
value={data.metrics.avgEngagement}
|
||||
change={data.metrics.engagementChange}
|
||||
value={data.metrics.exposure}
|
||||
change={data.metrics.exposureChange}
|
||||
icon={TrendingUp}
|
||||
suffix="%"
|
||||
delay={0.2}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Total Investment"
|
||||
value={data.metrics.totalInvestment}
|
||||
value={data.totalInvestment}
|
||||
icon={DollarSign}
|
||||
prefix="$"
|
||||
delay={0.3}
|
||||
@@ -423,7 +386,7 @@ export default function SponsorDashboardPage() {
|
||||
<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 ${
|
||||
league.tier === 'main'
|
||||
league.tier === 'main'
|
||||
? 'bg-primary-blue/20 text-primary-blue border border-primary-blue/30'
|
||||
: 'bg-purple-500/20 text-purple-400 border border-purple-500/30'
|
||||
}`}>
|
||||
@@ -432,17 +395,17 @@ export default function SponsorDashboardPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trophy className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium text-white">{league.name}</span>
|
||||
<span className="font-medium text-white">{league.entityName}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{league.drivers} drivers</div>
|
||||
<div className="text-sm text-gray-500">{league.details}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-white">{league.impressions.toLocaleString()}</div>
|
||||
<div className="font-semibold text-white">{league.formattedImpressions}</div>
|
||||
<div className="text-xs text-gray-500">impressions</div>
|
||||
</div>
|
||||
<Link href={`/sponsor/leagues/${league.id}`}>
|
||||
<Link href={`/sponsor/leagues/${league.entityId}`}>
|
||||
<Button variant="secondary" className="text-xs">
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</Button>
|
||||
@@ -450,7 +413,7 @@ export default function SponsorDashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
{/* Teams */}
|
||||
{data.sponsorships.teams.map((team) => (
|
||||
<div key={team.id} className="flex items-center justify-between p-4 hover:bg-iron-gray/30 transition-colors">
|
||||
@@ -461,14 +424,14 @@ export default function SponsorDashboardPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium text-white">{team.name}</span>
|
||||
<span className="font-medium text-white">{team.entityName}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{team.drivers} drivers</div>
|
||||
<div className="text-sm text-gray-500">{team.details}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-white">{team.impressions.toLocaleString()}</div>
|
||||
<div className="font-semibold text-white">{team.formattedImpressions}</div>
|
||||
<div className="text-xs text-gray-500">impressions</div>
|
||||
</div>
|
||||
<Button variant="secondary" className="text-xs">
|
||||
@@ -488,14 +451,14 @@ export default function SponsorDashboardPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Car className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium text-white">{driver.name}</span>
|
||||
<span className="font-medium text-white">{driver.entityName}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{driver.team}</div>
|
||||
<div className="text-sm text-gray-500">{driver.details}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-white">{driver.impressions.toLocaleString()}</div>
|
||||
<div className="font-semibold text-white">{driver.formattedImpressions}</div>
|
||||
<div className="text-xs text-gray-500">impressions</div>
|
||||
</div>
|
||||
<Button variant="secondary" className="text-xs">
|
||||
@@ -525,8 +488,8 @@ export default function SponsorDashboardPage() {
|
||||
<Flag className="w-5 h-5 text-warning-amber" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-white">{race.name}</p>
|
||||
<p className="text-sm text-gray-500">{race.league} • {race.date}</p>
|
||||
<p className="font-medium text-white">{race.entityName}</p>
|
||||
<p className="text-sm text-gray-500">{race.details}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@@ -620,16 +583,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.metrics.activeSponsors}</span>
|
||||
<span className="font-medium text-white">{data.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.metrics.totalInvestment.toLocaleString()}</span>
|
||||
<span className="font-medium text-white">{data.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.metrics.totalInvestment / data.metrics.totalImpressions * 1000).toFixed(2)}
|
||||
{data.costPerThousandViews}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } 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 { SponsorService } from '@/lib/services/sponsors/SponsorService';
|
||||
import { ServiceFactory } from '@/lib/services/ServiceFactory';
|
||||
import {
|
||||
Trophy,
|
||||
Users,
|
||||
@@ -29,64 +32,6 @@ import {
|
||||
FileText
|
||||
} from 'lucide-react';
|
||||
|
||||
// Mock data for league detail
|
||||
const MOCK_LEAGUE = {
|
||||
id: 'league-1',
|
||||
name: 'GT3 Masters Championship',
|
||||
game: 'iRacing',
|
||||
tier: 'premium' as const,
|
||||
season: 'Season 3',
|
||||
description: 'Premier GT3 racing with top-tier drivers competing across the world\'s most iconic circuits. Weekly broadcasts and an active community make this league a premium sponsorship opportunity.',
|
||||
drivers: 48,
|
||||
races: 12,
|
||||
completedRaces: 8,
|
||||
totalImpressions: 45200,
|
||||
avgViewsPerRace: 5650,
|
||||
engagement: 4.2,
|
||||
rating: 4.8,
|
||||
seasonStatus: 'active' as const,
|
||||
seasonDates: { start: '2025-10-01', end: '2026-02-28' },
|
||||
nextRace: { name: 'Spa-Francorchamps', date: '2025-12-20' },
|
||||
sponsorSlots: {
|
||||
main: {
|
||||
available: true,
|
||||
price: 1200,
|
||||
benefits: [
|
||||
'Primary logo placement on all liveries',
|
||||
'League page header banner',
|
||||
'Race results page branding',
|
||||
'Social media feature posts',
|
||||
'Newsletter sponsor spot',
|
||||
]
|
||||
},
|
||||
secondary: {
|
||||
available: 1,
|
||||
total: 2,
|
||||
price: 400,
|
||||
benefits: [
|
||||
'Secondary logo on liveries',
|
||||
'League page sidebar placement',
|
||||
'Race results mention',
|
||||
'Social media mentions',
|
||||
]
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_DRIVERS = [
|
||||
{ id: 'd1', name: 'Max Verstappen', country: 'NL', position: 1, races: 8, impressions: 4200, team: 'Red Bull Racing' },
|
||||
{ id: 'd2', name: 'Lewis Hamilton', country: 'GB', position: 2, races: 8, impressions: 3980, team: 'Mercedes AMG' },
|
||||
{ id: 'd3', name: 'Charles Leclerc', country: 'MC', position: 3, races: 8, impressions: 3750, team: 'Ferrari' },
|
||||
{ id: 'd4', name: 'Lando Norris', country: 'GB', position: 4, races: 7, impressions: 3420, team: 'McLaren' },
|
||||
{ id: 'd5', name: 'Carlos Sainz', country: 'ES', position: 5, races: 8, impressions: 3100, team: 'Ferrari' },
|
||||
];
|
||||
|
||||
const MOCK_RACES = [
|
||||
{ id: 'r1', name: 'Spa-Francorchamps', date: '2025-12-20', views: 0, status: 'upcoming' },
|
||||
{ id: 'r2', name: 'Monza', date: '2025-12-08', views: 5800, status: 'completed' },
|
||||
{ id: 'r3', name: 'Silverstone', date: '2025-11-24', views: 6200, status: 'completed' },
|
||||
{ id: 'r4', name: 'Nürburgring', date: '2025-11-10', views: 5400, status: 'completed' },
|
||||
];
|
||||
|
||||
type TabType = 'overview' | 'drivers' | 'races' | 'sponsor';
|
||||
|
||||
@@ -94,19 +39,57 @@ export default function SponsorLeagueDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
|
||||
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 league = MOCK_LEAGUE;
|
||||
const tierConfig = {
|
||||
premium: { color: 'text-yellow-400', bgColor: 'bg-yellow-500/10', border: 'border-yellow-500/30' },
|
||||
standard: { color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', border: 'border-primary-blue/30' },
|
||||
starter: { color: 'text-gray-400', bgColor: 'bg-gray-500/10', border: 'border-gray-500/30' },
|
||||
};
|
||||
const leagueId = params.id as string;
|
||||
|
||||
const config = tierConfig[league.tier];
|
||||
useEffect(() => {
|
||||
const loadLeagueDetail = async () => {
|
||||
try {
|
||||
const sponsorService = ServiceFactory.getSponsorService();
|
||||
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]);
|
||||
|
||||
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 league details...</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 league data available'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const config = data.league.tierConfig;
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-8 px-4">
|
||||
@@ -134,9 +117,9 @@ export default function SponsorLeagueDetailPage() {
|
||||
<span className="text-sm font-medium text-white">{league.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">{league.name}</h1>
|
||||
<p className="text-gray-400 mb-4">{league.game} • {league.season} • {league.completedRaces}/{league.races} races completed</p>
|
||||
<p className="text-gray-400 max-w-2xl">{league.description}</p>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">{data.league.name}</h1>
|
||||
<p className="text-gray-400 mb-4">{data.league.game} • {data.league.season} • {data.league.completedRaces}/{data.league.races} races completed</p>
|
||||
<p className="text-gray-400 max-w-2xl">{data.league.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
@@ -167,7 +150,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<Eye className="w-5 h-5 text-primary-blue" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-white">{league.totalImpressions.toLocaleString()}</div>
|
||||
<div className="text-xl font-bold text-white">{data.league.formattedTotalImpressions}</div>
|
||||
<div className="text-xs text-gray-400">Total Views</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,7 +167,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<TrendingUp className="w-5 h-5 text-performance-green" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-white">{league.avgViewsPerRace.toLocaleString()}</div>
|
||||
<div className="text-xl font-bold text-white">{data.league.formattedAvgViewsPerRace}</div>
|
||||
<div className="text-xs text-gray-400">Avg/Race</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,7 +184,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<Users className="w-5 h-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-white">{league.drivers}</div>
|
||||
<div className="text-xl font-bold text-white">{data.league.drivers}</div>
|
||||
<div className="text-xs text-gray-400">Drivers</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +201,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<BarChart3 className="w-5 h-5 text-warning-amber" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-white">{league.engagement}%</div>
|
||||
<div className="text-xl font-bold text-white">{data.league.engagement}%</div>
|
||||
<div className="text-xs text-gray-400">Engagement</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,7 +218,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<Calendar className="w-5 h-5 text-racing-red" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-white">{league.races - league.completedRaces}</div>
|
||||
<div className="text-xl font-bold text-white">{data.league.racesLeft}</div>
|
||||
<div className="text-xs text-gray-400">Races Left</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,7 +254,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
|
||||
<span className="text-gray-400">Platform</span>
|
||||
<span className="text-white font-medium">{league.game}</span>
|
||||
<span className="text-white font-medium">{data.league.game}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
|
||||
<span className="text-gray-400">Season</span>
|
||||
@@ -300,16 +283,16 @@ export default function SponsorLeagueDetailPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
|
||||
<span className="text-gray-400">Total Season Views</span>
|
||||
<span className="text-white font-medium">{league.totalImpressions.toLocaleString()}</span>
|
||||
<span className="text-white font-medium">{data.league.formattedTotalImpressions}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
|
||||
<span className="text-gray-400">Projected Total</span>
|
||||
<span className="text-white font-medium">{Math.round(league.avgViewsPerRace * league.races).toLocaleString()}</span>
|
||||
<span className="text-white font-medium">{data.league.formattedProjectedTotal}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
|
||||
<span className="text-gray-400">Main Sponsor CPM</span>
|
||||
<span className="text-performance-green font-medium">
|
||||
${((league.sponsorSlots.main.price / (league.avgViewsPerRace * league.races)) * 1000).toFixed(2)}
|
||||
{data.league.formattedMainSponsorCpm}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-charcoal-outline/50">
|
||||
@@ -359,7 +342,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<p className="text-sm text-gray-400">Top drivers carrying sponsor branding</p>
|
||||
</div>
|
||||
<div className="divide-y divide-charcoal-outline/50">
|
||||
{MOCK_DRIVERS.map((driver) => (
|
||||
{data.drivers.map((driver) => (
|
||||
<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="w-10 h-10 rounded-full bg-iron-gray flex items-center justify-center text-lg font-bold text-white">
|
||||
@@ -376,7 +359,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<div className="text-xs text-gray-500">races</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-white">{driver.impressions.toLocaleString()}</div>
|
||||
<div className="font-semibold text-white">{driver.formattedImpressions}</div>
|
||||
<div className="text-xs text-gray-500">views</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -393,7 +376,7 @@ export default function SponsorLeagueDetailPage() {
|
||||
<p className="text-sm text-gray-400">Season schedule with view statistics</p>
|
||||
</div>
|
||||
<div className="divide-y divide-charcoal-outline/50">
|
||||
{MOCK_RACES.map((race) => (
|
||||
{data.races.map((race) => (
|
||||
<div key={race.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={`w-3 h-3 rounded-full ${
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
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 { SponsorService } from '@/lib/services/sponsors/SponsorService';
|
||||
import { ServiceFactory } from '@/lib/services/ServiceFactory';
|
||||
import {
|
||||
Trophy,
|
||||
Users,
|
||||
@@ -38,98 +41,12 @@ interface AvailableLeague {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const MOCK_AVAILABLE_LEAGUES: AvailableLeague[] = [
|
||||
{
|
||||
id: 'league-1',
|
||||
name: 'GT3 Masters Championship',
|
||||
game: 'iRacing',
|
||||
drivers: 48,
|
||||
avgViewsPerRace: 8200,
|
||||
mainSponsorSlot: { available: true, price: 1200 },
|
||||
secondarySlots: { available: 1, total: 2, price: 400 },
|
||||
rating: 4.8,
|
||||
tier: 'premium',
|
||||
nextRace: 'Dec 20 - Spa',
|
||||
seasonStatus: 'active',
|
||||
description: 'Premier GT3 racing with top-tier drivers. Weekly broadcasts and active community.',
|
||||
},
|
||||
{
|
||||
id: 'league-2',
|
||||
name: 'Endurance Pro Series',
|
||||
game: 'ACC',
|
||||
drivers: 72,
|
||||
avgViewsPerRace: 12500,
|
||||
mainSponsorSlot: { available: false, price: 1500 },
|
||||
secondarySlots: { available: 2, total: 2, price: 500 },
|
||||
rating: 4.9,
|
||||
tier: 'premium',
|
||||
nextRace: 'Jan 5 - Nürburgring 24h',
|
||||
seasonStatus: 'active',
|
||||
description: 'Multi-class endurance racing. High engagement from dedicated endurance fans.',
|
||||
},
|
||||
{
|
||||
id: 'league-3',
|
||||
name: 'Formula Sim League',
|
||||
game: 'iRacing',
|
||||
drivers: 24,
|
||||
avgViewsPerRace: 5400,
|
||||
mainSponsorSlot: { available: true, price: 800 },
|
||||
secondarySlots: { available: 2, total: 2, price: 300 },
|
||||
rating: 4.5,
|
||||
tier: 'standard',
|
||||
nextRace: 'Dec 22 - Monza',
|
||||
seasonStatus: 'active',
|
||||
description: 'Open-wheel racing excellence. Competitive field with consistent racing.',
|
||||
},
|
||||
{
|
||||
id: 'league-4',
|
||||
name: 'Touring Car Masters',
|
||||
game: 'rFactor 2',
|
||||
drivers: 32,
|
||||
avgViewsPerRace: 3200,
|
||||
mainSponsorSlot: { available: true, price: 500 },
|
||||
secondarySlots: { available: 2, total: 2, price: 200 },
|
||||
rating: 4.2,
|
||||
tier: 'starter',
|
||||
nextRace: 'Jan 10 - Brands Hatch',
|
||||
seasonStatus: 'upcoming',
|
||||
description: 'Touring car action with close racing. Great for building brand awareness.',
|
||||
},
|
||||
{
|
||||
id: 'league-5',
|
||||
name: 'LMP Challenge',
|
||||
game: 'Le Mans Ultimate',
|
||||
drivers: 36,
|
||||
avgViewsPerRace: 6800,
|
||||
mainSponsorSlot: { available: true, price: 900 },
|
||||
secondarySlots: { available: 1, total: 2, price: 350 },
|
||||
rating: 4.6,
|
||||
tier: 'standard',
|
||||
nextRace: 'Dec 28 - Sebring',
|
||||
seasonStatus: 'active',
|
||||
description: 'Prototype racing at its finest. Growing community with passionate fans.',
|
||||
},
|
||||
{
|
||||
id: 'league-6',
|
||||
name: 'Rally Championship',
|
||||
game: 'EA WRC',
|
||||
drivers: 28,
|
||||
avgViewsPerRace: 4500,
|
||||
mainSponsorSlot: { available: true, price: 650 },
|
||||
secondarySlots: { available: 2, total: 2, price: 250 },
|
||||
rating: 4.4,
|
||||
tier: 'standard',
|
||||
nextRace: 'Jan 15 - Monte Carlo',
|
||||
seasonStatus: 'upcoming',
|
||||
description: 'Thrilling rally stages. Unique sponsorship exposure in rallying content.',
|
||||
},
|
||||
];
|
||||
|
||||
type SortOption = 'rating' | 'drivers' | 'price' | 'views';
|
||||
type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
|
||||
type AvailabilityFilter = 'all' | 'main' | 'secondary';
|
||||
|
||||
function LeagueCard({ league, index }: { league: AvailableLeague; index: number }) {
|
||||
function LeagueCard({ league, index }: { league: any; index: number }) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const tierConfig = {
|
||||
@@ -159,9 +76,8 @@ function LeagueCard({ league, index }: { league: AvailableLeague; index: number
|
||||
completed: { color: 'text-gray-400', bg: 'bg-gray-400/10', label: 'Season Ended' },
|
||||
};
|
||||
|
||||
const config = tierConfig[league.tier];
|
||||
const status = statusConfig[league.seasonStatus];
|
||||
const cpm = (league.mainSponsorSlot.price / league.avgViewsPerRace * 1000).toFixed(0);
|
||||
const config = league.tierConfig;
|
||||
const status = league.statusConfig;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -201,11 +117,11 @@ function LeagueCard({ league, index }: { league: AvailableLeague; index: number
|
||||
<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.avgViewsPerRace / 1000).toFixed(1)}k</div>
|
||||
<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">${cpm}</div>
|
||||
<div className="text-lg font-bold text-performance-green">{league.formattedCpm}</div>
|
||||
<div className="text-xs text-gray-500">CPM</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,9 +198,50 @@ export default function SponsorLeaguesPage() {
|
||||
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 sponsorService = ServiceFactory.getSponsorService();
|
||||
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();
|
||||
}, []);
|
||||
|
||||
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 = MOCK_AVAILABLE_LEAGUES
|
||||
const filteredLeagues = data.leagues
|
||||
.filter(league => {
|
||||
if (searchQuery && !league.name.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
return false;
|
||||
@@ -312,13 +269,12 @@ export default function SponsorLeaguesPage() {
|
||||
|
||||
// Calculate summary stats
|
||||
const stats = {
|
||||
total: MOCK_AVAILABLE_LEAGUES.length,
|
||||
mainAvailable: MOCK_AVAILABLE_LEAGUES.filter(l => l.mainSponsorSlot.available).length,
|
||||
secondaryAvailable: MOCK_AVAILABLE_LEAGUES.reduce((sum, l) => sum + l.secondarySlots.available, 0),
|
||||
totalDrivers: MOCK_AVAILABLE_LEAGUES.reduce((sum, l) => sum + l.drivers, 0),
|
||||
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(
|
||||
MOCK_AVAILABLE_LEAGUES.reduce((sum, l) => sum + (l.mainSponsorSlot.price / l.avgViewsPerRace * 1000), 0) /
|
||||
MOCK_AVAILABLE_LEAGUES.length
|
||||
data.leagues.reduce((sum, l) => sum + l.cpm, 0) / data.leagues.length
|
||||
),
|
||||
};
|
||||
|
||||
@@ -448,7 +404,7 @@ export default function SponsorLeaguesPage() {
|
||||
{/* Results Count */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<p className="text-sm text-gray-400">
|
||||
Showing {filteredLeagues.length} of {MOCK_AVAILABLE_LEAGUES.length} leagues
|
||||
Showing {filteredLeagues.length} of {data.leagues.length} leagues
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/teams">
|
||||
|
||||
Reference in New Issue
Block a user