This commit is contained in:
2025-12-10 12:38:55 +01:00
parent 0f7fe67d3c
commit fbbcf414a4
87 changed files with 11972 additions and 390 deletions

View File

@@ -0,0 +1,164 @@
'use client';
import { useState } from 'react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { Paintbrush, Upload, Car, Download, Trash2, Edit } from 'lucide-react';
import Link from 'next/link';
interface DriverLiveryItem {
id: string;
carId: string;
carName: string;
thumbnailUrl: string;
uploadedAt: Date;
isValidated: boolean;
}
export default function DriverLiveriesPage() {
const [liveries] = useState<DriverLiveryItem[]>([]);
return (
<div className="max-w-4xl mx-auto py-12">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-blue/10">
<Paintbrush className="w-6 h-6 text-primary-blue" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">My Liveries</h1>
<p className="text-sm text-gray-400">Manage your car liveries across leagues</p>
</div>
</div>
<Link href="/profile/liveries/upload">
<Button variant="primary">
<Upload className="w-4 h-4 mr-2" />
Upload Livery
</Button>
</Link>
</div>
{/* Livery Collection */}
{liveries.length === 0 ? (
<Card>
<div className="text-center py-16">
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-iron-gray/50 flex items-center justify-center">
<Car className="w-10 h-10 text-gray-500" />
</div>
<h3 className="text-xl font-medium text-white mb-3">No Liveries Yet</h3>
<p className="text-sm text-gray-400 max-w-md mx-auto mb-6">
Upload your first livery. Use the same livery across multiple leagues or create custom ones for each.
</p>
<Link href="/profile/liveries/upload">
<Button variant="primary">
<Upload className="w-4 h-4 mr-2" />
Upload Your First Livery
</Button>
</Link>
</div>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{liveries.map((livery) => (
<Card key={livery.id} className="overflow-hidden hover:border-primary-blue/50 transition-colors">
{/* Livery Preview */}
<div className="aspect-video bg-deep-graphite rounded-lg mb-4 flex items-center justify-center border border-charcoal-outline">
<Car className="w-16 h-16 text-gray-600" />
</div>
{/* Livery Info */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-white">{livery.carName}</h3>
{livery.isValidated ? (
<span className="px-2 py-0.5 text-xs bg-performance-green/10 text-performance-green border border-performance-green/30 rounded-full">
Validated
</span>
) : (
<span className="px-2 py-0.5 text-xs bg-warning-amber/10 text-warning-amber border border-warning-amber/30 rounded-full">
Pending
</span>
)}
</div>
<p className="text-xs text-gray-500">
Uploaded {new Date(livery.uploadedAt).toLocaleDateString()}
</p>
{/* Actions */}
<div className="flex gap-2 pt-2">
<Button variant="secondary" className="flex-1 px-3 py-1.5">
<Edit className="w-4 h-4 mr-1" />
Edit
</Button>
<Button variant="secondary" className="px-3 py-1.5">
<Download className="w-4 h-4" />
</Button>
<Button variant="danger" className="px-3 py-1.5">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
))}
</div>
)}
{/* Info Section */}
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<h3 className="text-lg font-semibold text-white mb-3">Livery Requirements</h3>
<ul className="space-y-2 text-sm text-gray-400">
<li className="flex items-start gap-2">
<span className="text-primary-blue mt-0.5"></span>
PNG or DDS format, max 5MB
</li>
<li className="flex items-start gap-2">
<span className="text-primary-blue mt-0.5"></span>
No logos or text allowed on base livery
</li>
<li className="flex items-start gap-2">
<span className="text-primary-blue mt-0.5"></span>
Sponsor decals are added by league admins
</li>
<li className="flex items-start gap-2">
<span className="text-primary-blue mt-0.5"></span>
Your driver name and number are added automatically
</li>
</ul>
</Card>
<Card>
<h3 className="text-lg font-semibold text-white mb-3">How It Works</h3>
<ol className="space-y-2 text-sm text-gray-400">
<li className="flex items-start gap-2">
<span className="text-primary-blue font-semibold">1.</span>
Upload your base livery for each car you race
</li>
<li className="flex items-start gap-2">
<span className="text-primary-blue font-semibold">2.</span>
Position your name and number decals
</li>
<li className="flex items-start gap-2">
<span className="text-primary-blue font-semibold">3.</span>
League admins add sponsor logos
</li>
<li className="flex items-start gap-2">
<span className="text-primary-blue font-semibold">4.</span>
Download the final pack with all decals burned in
</li>
</ol>
</Card>
</div>
{/* Alpha Notice */}
<div className="mt-6 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> Livery management is demonstration-only.
In production, liveries are stored in cloud storage and composited with sponsor decals.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,405 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { Upload, Paintbrush, Move, ZoomIn, Check, X, AlertTriangle, Car, RotateCw, Gamepad2 } from 'lucide-react';
interface DecalPosition {
id: string;
type: 'name' | 'number' | 'rank';
x: number;
y: number;
width: number;
height: number;
rotation: number;
}
interface GameOption {
id: string;
name: string;
}
interface CarOption {
id: string;
name: string;
manufacturer: string;
gameId: string;
}
// Mock data - in production these would come from API
const GAMES: GameOption[] = [
{ id: 'iracing', name: 'iRacing' },
{ id: 'acc', name: 'Assetto Corsa Competizione' },
{ id: 'ac', name: 'Assetto Corsa' },
{ id: 'rf2', name: 'rFactor 2' },
{ id: 'ams2', name: 'Automobilista 2' },
{ id: 'lmu', name: 'Le Mans Ultimate' },
];
const CARS: CarOption[] = [
// iRacing cars
{ id: 'ir-porsche-911-gt3r', name: '911 GT3 R', manufacturer: 'Porsche', gameId: 'iracing' },
{ id: 'ir-ferrari-296-gt3', name: '296 GT3', manufacturer: 'Ferrari', gameId: 'iracing' },
{ id: 'ir-bmw-m4-gt3', name: 'M4 GT3', manufacturer: 'BMW', gameId: 'iracing' },
{ id: 'ir-mercedes-amg-gt3', name: 'AMG GT3 Evo', manufacturer: 'Mercedes-AMG', gameId: 'iracing' },
{ id: 'ir-audi-r8-gt3', name: 'R8 LMS GT3 Evo II', manufacturer: 'Audi', gameId: 'iracing' },
{ id: 'ir-dallara-f3', name: 'F3', manufacturer: 'Dallara', gameId: 'iracing' },
{ id: 'ir-dallara-ir18', name: 'IR-18', manufacturer: 'Dallara', gameId: 'iracing' },
// ACC cars
{ id: 'acc-porsche-911-gt3r', name: '911 GT3 R', manufacturer: 'Porsche', gameId: 'acc' },
{ id: 'acc-ferrari-296-gt3', name: '296 GT3', manufacturer: 'Ferrari', gameId: 'acc' },
{ id: 'acc-bmw-m4-gt3', name: 'M4 GT3', manufacturer: 'BMW', gameId: 'acc' },
{ id: 'acc-mercedes-amg-gt3', name: 'AMG GT3 Evo', manufacturer: 'Mercedes-AMG', gameId: 'acc' },
{ id: 'acc-lamborghini-huracan-gt3', name: 'Huracán GT3 Evo2', manufacturer: 'Lamborghini', gameId: 'acc' },
{ id: 'acc-aston-martin-v8-gt3', name: 'V8 Vantage GT3', manufacturer: 'Aston Martin', gameId: 'acc' },
// AC cars
{ id: 'ac-porsche-911-gt3r', name: '911 GT3 R', manufacturer: 'Porsche', gameId: 'ac' },
{ id: 'ac-ferrari-488-gt3', name: '488 GT3', manufacturer: 'Ferrari', gameId: 'ac' },
{ id: 'ac-lotus-exos', name: 'Exos 125', manufacturer: 'Lotus', gameId: 'ac' },
// rFactor 2 cars
{ id: 'rf2-porsche-911-gt3r', name: '911 GT3 R', manufacturer: 'Porsche', gameId: 'rf2' },
{ id: 'rf2-bmw-m4-gt3', name: 'M4 GT3', manufacturer: 'BMW', gameId: 'rf2' },
// AMS2 cars
{ id: 'ams2-porsche-911-gt3r', name: '911 GT3 R', manufacturer: 'Porsche', gameId: 'ams2' },
{ id: 'ams2-mclaren-720s-gt3', name: '720S GT3', manufacturer: 'McLaren', gameId: 'ams2' },
// LMU cars
{ id: 'lmu-porsche-963', name: '963 LMDh', manufacturer: 'Porsche', gameId: 'lmu' },
{ id: 'lmu-ferrari-499p', name: '499P', manufacturer: 'Ferrari', gameId: 'lmu' },
{ id: 'lmu-toyota-gr010', name: 'GR010', manufacturer: 'Toyota', gameId: 'lmu' },
];
export default function LiveryUploadPage() {
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [selectedGame, setSelectedGame] = useState<string>('');
const [selectedCar, setSelectedCar] = useState<string>('');
const [filteredCars, setFilteredCars] = useState<CarOption[]>([]);
const [decals, setDecals] = useState<DecalPosition[]>([
{ id: 'name', type: 'name', x: 0.1, y: 0.8, width: 0.2, height: 0.05, rotation: 0 },
{ id: 'number', type: 'number', x: 0.8, y: 0.1, width: 0.15, height: 0.15, rotation: 0 },
{ id: 'rank', type: 'rank', x: 0.05, y: 0.1, width: 0.1, height: 0.1, rotation: 0 },
]);
const [activeDecal, setActiveDecal] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
// Filter cars when game changes
useEffect(() => {
if (selectedGame) {
const cars = CARS.filter(car => car.gameId === selectedGame);
setFilteredCars(cars);
setSelectedCar(''); // Reset car selection when game changes
} else {
setFilteredCars([]);
setSelectedCar('');
}
}, [selectedGame]);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setUploadedFile(file);
const url = URL.createObjectURL(file);
setPreviewUrl(url);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const file = e.dataTransfer.files?.[0];
if (file) {
setUploadedFile(file);
const url = URL.createObjectURL(file);
setPreviewUrl(url);
}
};
const handleSubmit = async () => {
if (!uploadedFile || !selectedGame || !selectedCar) return;
setSubmitting(true);
try {
// Alpha: In-memory only
console.log('Livery upload:', {
file: uploadedFile.name,
gameId: selectedGame,
carId: selectedCar,
decals,
});
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Livery uploaded successfully.');
router.push('/profile/liveries');
} catch (err) {
console.error('Upload failed:', err);
alert('Upload failed. Try again.');
} finally {
setSubmitting(false);
}
};
return (
<div className="max-w-4xl mx-auto py-12">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-blue/10">
<Upload className="w-6 h-6 text-primary-blue" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Upload Livery</h1>
<p className="text-sm text-gray-400">Add a new livery to your collection</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Upload Section */}
<Card>
<h2 className="text-lg font-semibold text-white mb-4">Livery File</h2>
{/* Game Selection */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-300 mb-2">
<div className="flex items-center gap-2">
<Gamepad2 className="w-4 h-4" />
Select Game
</div>
</label>
<select
value={selectedGame}
onChange={(e) => setSelectedGame(e.target.value)}
className="w-full rounded-lg border border-charcoal-outline bg-iron-gray px-3 py-2 text-sm text-white focus:border-primary-blue focus:outline-none"
>
<option value="">Choose a game...</option>
{GAMES.map((game) => (
<option key={game.id} value={game.id}>
{game.name}
</option>
))}
</select>
</div>
{/* Car Selection */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">
<div className="flex items-center gap-2">
<Car className="w-4 h-4" />
Select Car
</div>
</label>
<select
value={selectedCar}
onChange={(e) => setSelectedCar(e.target.value)}
disabled={!selectedGame}
className="w-full rounded-lg border border-charcoal-outline bg-iron-gray px-3 py-2 text-sm text-white focus:border-primary-blue focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value="">{selectedGame ? 'Choose a car...' : 'Select a game first...'}</option>
{filteredCars.map((car) => (
<option key={car.id} value={car.id}>
{car.manufacturer} {car.name}
</option>
))}
</select>
{selectedGame && filteredCars.length === 0 && (
<p className="text-xs text-gray-500 mt-1">No cars available for this game</p>
)}
</div>
{/* File Upload */}
<div
onClick={() => fileInputRef.current?.click()}
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
previewUrl
? 'border-performance-green/50 bg-performance-green/5'
: 'border-charcoal-outline hover:border-primary-blue/50 hover:bg-primary-blue/5'
}`}
>
<input
ref={fileInputRef}
type="file"
accept=".png,.dds"
onChange={handleFileChange}
className="hidden"
/>
{previewUrl ? (
<div className="space-y-3">
<Check className="w-12 h-12 text-performance-green mx-auto" />
<p className="text-sm text-white font-medium">{uploadedFile?.name}</p>
<p className="text-xs text-gray-500">Click to replace</p>
</div>
) : (
<div className="space-y-3">
<Upload className="w-12 h-12 text-gray-500 mx-auto" />
<p className="text-sm text-gray-400">
Drop your livery here or click to browse
</p>
<p className="text-xs text-gray-500">PNG or DDS, max 5MB</p>
</div>
)}
</div>
{/* Validation Warning */}
<div className="mt-4 p-3 rounded-lg bg-warning-amber/10 border border-warning-amber/30">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-warning-amber shrink-0 mt-0.5" />
<p className="text-xs text-gray-400">
<strong className="text-warning-amber">No logos or text allowed.</strong>{' '}
Your base livery must be clean. Sponsor logos are added by league admins.
</p>
</div>
</div>
</Card>
{/* Decal Editor */}
<Card>
<h2 className="text-lg font-semibold text-white mb-4">Position Decals</h2>
<p className="text-sm text-gray-400 mb-4">
Drag to position your driver name, number, and rank badge.
</p>
{/* Preview Canvas */}
<div className="relative aspect-video bg-deep-graphite rounded-lg border border-charcoal-outline overflow-hidden mb-4">
{previewUrl ? (
<img
src={previewUrl}
alt="Livery preview"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Car className="w-20 h-20 text-gray-600" />
</div>
)}
{/* Decal Placeholders */}
{decals.map((decal) => (
<div
key={decal.id}
onClick={() => setActiveDecal(decal.id === activeDecal ? null : decal.id)}
className={`absolute cursor-move border-2 rounded flex items-center justify-center text-xs font-medium transition-all ${
activeDecal === decal.id
? 'border-primary-blue bg-primary-blue/20 text-primary-blue'
: 'border-white/30 bg-black/30 text-white/70'
}`}
style={{
left: `${decal.x * 100}%`,
top: `${decal.y * 100}%`,
width: `${decal.width * 100}%`,
height: `${decal.height * 100}%`,
transform: `rotate(${decal.rotation}deg)`,
}}
>
{decal.type === 'name' && 'NAME'}
{decal.type === 'number' && '#'}
{decal.type === 'rank' && 'RANK'}
</div>
))}
</div>
{/* Decal Controls */}
<div className="grid grid-cols-3 gap-3">
{decals.map((decal) => (
<button
key={decal.id}
onClick={() => setActiveDecal(decal.id === activeDecal ? null : decal.id)}
className={`p-3 rounded-lg border text-center transition-all ${
activeDecal === decal.id
? 'border-primary-blue bg-primary-blue/10 text-primary-blue'
: 'border-charcoal-outline bg-iron-gray/30 text-gray-400 hover:border-primary-blue/50'
}`}
>
<div className="text-xs font-medium capitalize mb-1">{decal.type}</div>
<div className="text-xs text-gray-500">
{Math.round(decal.x * 100)}%, {Math.round(decal.y * 100)}%
</div>
<div className="text-xs text-gray-600">
{decal.rotation}°
</div>
</button>
))}
</div>
{/* Rotation Controls */}
{activeDecal && (
<div className="mt-4 p-3 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-300 capitalize">
{decals.find(d => d.id === activeDecal)?.type} Rotation
</span>
<span className="text-xs text-gray-500">
{decals.find(d => d.id === activeDecal)?.rotation}°
</span>
</div>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="360"
step="15"
value={decals.find(d => d.id === activeDecal)?.rotation ?? 0}
onChange={(e) => {
const rotation = parseInt(e.target.value, 10);
setDecals(decals.map(d =>
d.id === activeDecal ? { ...d, rotation } : d
));
}}
className="flex-1 h-2 bg-charcoal-outline rounded-lg appearance-none cursor-pointer accent-primary-blue"
/>
<button
onClick={() => {
setDecals(decals.map(d =>
d.id === activeDecal ? { ...d, rotation: (d.rotation + 90) % 360 } : d
));
}}
className="p-2 rounded-lg border border-charcoal-outline bg-iron-gray/30 hover:bg-iron-gray/50 transition-colors"
title="Rotate 90°"
>
<RotateCw className="w-4 h-4 text-gray-400" />
</button>
</div>
</div>
)}
<p className="text-xs text-gray-500 mt-4">
Click a decal above, then drag on preview to reposition. Use the slider or button to rotate.
</p>
</Card>
</div>
{/* Actions */}
<div className="mt-6 flex gap-3">
<Button
variant="primary"
onClick={handleSubmit}
disabled={!uploadedFile || !selectedGame || !selectedCar || submitting}
>
{submitting ? 'Uploading...' : 'Upload Livery'}
</Button>
<Button
variant="secondary"
onClick={() => router.back()}
disabled={submitting}
>
Cancel
</Button>
</div>
{/* Alpha Notice */}
<div className="mt-6 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> Livery upload is demonstration-only.
Decal positioning and image validation are not functional in this preview.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,303 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import PendingSponsorshipRequests, { type PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
import {
getGetPendingSponsorshipRequestsQuery,
getAcceptSponsorshipRequestUseCase,
getRejectSponsorshipRequestUseCase,
getDriverRepository,
getLeagueRepository,
getTeamRepository,
getLeagueMembershipRepository,
getTeamMembershipRepository,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
import { Handshake, User, Users, Trophy, ChevronRight, Building, AlertTriangle } from 'lucide-react';
import Link from 'next/link';
interface EntitySection {
entityType: 'driver' | 'team' | 'race' | 'season';
entityId: string;
entityName: string;
requests: PendingRequestDTO[];
}
export default function SponsorshipRequestsPage() {
const router = useRouter();
const currentDriverId = useEffectiveDriverId();
const [sections, setSections] = useState<EntitySection[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadAllRequests = useCallback(async () => {
setLoading(true);
setError(null);
try {
const driverRepo = getDriverRepository();
const leagueRepo = getLeagueRepository();
const teamRepo = getTeamRepository();
const leagueMembershipRepo = getLeagueMembershipRepository();
const teamMembershipRepo = getTeamMembershipRepository();
const query = getGetPendingSponsorshipRequestsQuery();
const allSections: EntitySection[] = [];
// 1. Driver's own sponsorship requests
const driverResult = await query.execute({
entityType: 'driver',
entityId: currentDriverId,
});
if (driverResult.requests.length > 0) {
const driver = await driverRepo.findById(currentDriverId);
allSections.push({
entityType: 'driver',
entityId: currentDriverId,
entityName: driver?.name ?? 'Your Profile',
requests: driverResult.requests,
});
}
// 2. Leagues where the user is admin/owner
const allLeagues = await leagueRepo.findAll();
for (const league of allLeagues) {
const membership = await leagueMembershipRepo.getMembership(league.id, currentDriverId);
if (membership && isLeagueAdminOrHigherRole(membership.role)) {
// Load sponsorship requests for this league's active season
try {
// For simplicity, we'll query by season entityType - in production you'd get the active season ID
const leagueResult = await query.execute({
entityType: 'season',
entityId: league.id, // Using league ID as a proxy for now
});
if (leagueResult.requests.length > 0) {
allSections.push({
entityType: 'season',
entityId: league.id,
entityName: league.name,
requests: leagueResult.requests,
});
}
} catch (err) {
// Silently skip if no requests found
}
}
}
// 3. Teams where the user is owner/manager
const allTeams = await teamRepo.findAll();
for (const team of allTeams) {
const membership = await teamMembershipRepo.getMembership(team.id, currentDriverId);
if (membership && (membership.role === 'owner' || membership.role === 'manager')) {
const teamResult = await query.execute({
entityType: 'team',
entityId: team.id,
});
if (teamResult.requests.length > 0) {
allSections.push({
entityType: 'team',
entityId: team.id,
entityName: team.name,
requests: teamResult.requests,
});
}
}
}
setSections(allSections);
} catch (err) {
console.error('Failed to load sponsorship requests:', err);
setError(err instanceof Error ? err.message : 'Failed to load requests');
} finally {
setLoading(false);
}
}, [currentDriverId]);
useEffect(() => {
loadAllRequests();
}, [loadAllRequests]);
const handleAccept = async (requestId: string) => {
const useCase = getAcceptSponsorshipRequestUseCase();
await useCase.execute({
requestId,
respondedBy: currentDriverId,
});
await loadAllRequests();
};
const handleReject = async (requestId: string, reason?: string) => {
const useCase = getRejectSponsorshipRequestUseCase();
await useCase.execute({
requestId,
respondedBy: currentDriverId,
reason,
});
await loadAllRequests();
};
const getEntityIcon = (type: 'driver' | 'team' | 'race' | 'season') => {
switch (type) {
case 'driver':
return User;
case 'team':
return Users;
case 'race':
return Trophy;
case 'season':
return Trophy;
default:
return Building;
}
};
const getEntityLink = (type: 'driver' | 'team' | 'race' | 'season', id: string) => {
switch (type) {
case 'driver':
return `/drivers/${id}`;
case 'team':
return `/teams/${id}`;
case 'race':
return `/races/${id}`;
case 'season':
return `/leagues/${id}/sponsorships`;
default:
return '#';
}
};
const totalRequests = sections.reduce((sum, s) => sum + s.requests.length, 0);
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<Breadcrumbs
items={[
{ label: 'Profile', href: '/profile' },
{ label: 'Sponsorship Requests' },
]}
/>
{/* Header */}
<div className="flex items-center gap-4 mt-6 mb-8">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-performance-green/10 border border-performance-green/30">
<Handshake className="w-7 h-7 text-performance-green" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Sponsorship Requests</h1>
<p className="text-sm text-gray-400">
Manage sponsorship requests for your profile, teams, and leagues
</p>
</div>
{totalRequests > 0 && (
<div className="ml-auto px-3 py-1 rounded-full bg-performance-green/20 text-performance-green text-sm font-semibold">
{totalRequests} pending
</div>
)}
</div>
{loading ? (
<Card>
<div className="text-center py-12 text-gray-400">
<div className="animate-pulse">Loading sponsorship requests...</div>
</div>
</Card>
) : error ? (
<Card>
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-red-500/10 flex items-center justify-center">
<AlertTriangle className="w-8 h-8 text-red-400" />
</div>
<h3 className="text-lg font-medium text-white mb-2">Error Loading Requests</h3>
<p className="text-sm text-gray-400">{error}</p>
<Button variant="secondary" onClick={loadAllRequests} className="mt-4">
Try Again
</Button>
</div>
</Card>
) : sections.length === 0 ? (
<Card>
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-iron-gray/50 flex items-center justify-center">
<Handshake className="w-8 h-8 text-gray-500" />
</div>
<h3 className="text-lg font-medium text-white mb-2">No Pending Requests</h3>
<p className="text-sm text-gray-400">
You don't have any pending sponsorship requests at the moment.
</p>
<p className="text-xs text-gray-500 mt-2">
Sponsors can apply to sponsor your profile, teams, or leagues you manage.
</p>
</div>
</Card>
) : (
<div className="space-y-6">
{sections.map((section) => {
const Icon = getEntityIcon(section.entityType);
const entityLink = getEntityLink(section.entityType, section.entityId);
return (
<Card key={`${section.entityType}-${section.entityId}`}>
{/* Section Header */}
<div className="flex items-center justify-between mb-6 pb-4 border-b border-charcoal-outline">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-iron-gray/50">
<Icon className="w-5 h-5 text-gray-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">{section.entityName}</h2>
<p className="text-xs text-gray-500 capitalize">{section.entityType}</p>
</div>
</div>
<Link
href={entityLink}
className="flex items-center gap-1 text-sm text-primary-blue hover:text-primary-blue/80 transition-colors"
>
View {section.entityType === 'season' ? 'Sponsorships' : section.entityType}
<ChevronRight className="w-4 h-4" />
</Link>
</div>
{/* Requests */}
<PendingSponsorshipRequests
entityType={section.entityType}
entityId={section.entityId}
entityName={section.entityName}
requests={section.requests}
onAccept={handleAccept}
onReject={handleReject}
/>
</Card>
);
})}
</div>
)}
{/* Info Card */}
<Card className="mt-8 bg-gradient-to-r from-primary-blue/5 to-transparent border-primary-blue/20">
<div className="flex items-start gap-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/20 flex-shrink-0">
<Building className="w-5 h-5 text-primary-blue" />
</div>
<div>
<h3 className="text-sm font-semibold text-white mb-1">How Sponsorships Work</h3>
<p className="text-xs text-gray-400 leading-relaxed">
Sponsors can apply to sponsor your driver profile, teams you manage, or leagues you administer.
Review each request carefully - accepting will activate the sponsorship and the sponsor will be
charged. You'll receive the payment minus a 10% platform fee.
</p>
</div>
</div>
</Card>
</div>
);
}