Files
gridpilot.gg/apps/website/app/profile/sponsorship-requests/page.tsx
2025-12-11 21:06:25 +01:00

322 lines
12 KiB
TypeScript

'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 {
getGetPendingSponsorshipRequestsUseCase,
getAcceptSponsorshipRequestUseCase,
getRejectSponsorshipRequestUseCase,
getDriverRepository,
getLeagueRepository,
getTeamRepository,
getLeagueMembershipRepository,
getTeamMembershipRepository,
} from '@/lib/di-container';
import { PendingSponsorshipRequestsPresenter } from '@/lib/presenters/PendingSponsorshipRequestsPresenter';
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 useCase = getGetPendingSponsorshipRequestsUseCase();
const allSections: EntitySection[] = [];
// 1. Driver's own sponsorship requests
const driverPresenter = new PendingSponsorshipRequestsPresenter();
await useCase.execute(
{
entityType: 'driver',
entityId: currentDriverId,
},
driverPresenter,
);
const driverResult = driverPresenter.getViewModel();
if (driverResult && 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 leaguePresenter = new PendingSponsorshipRequestsPresenter();
await useCase.execute(
{
entityType: 'season',
entityId: league.id, // Using league ID as a proxy for now
},
leaguePresenter,
);
const leagueResult = leaguePresenter.getViewModel();
if (leagueResult && 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 teamPresenter = new PendingSponsorshipRequestsPresenter();
await useCase.execute(
{
entityType: 'team',
entityId: team.id,
},
teamPresenter,
);
const teamResult = teamPresenter.getViewModel();
if (teamResult && 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();
const input: { requestId: string; respondedBy: string; reason?: string } = {
requestId,
respondedBy: currentDriverId,
};
if (typeof reason === 'string') {
input.reason = reason;
}
await useCase.execute(input);
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>
);
}