241 lines
8.9 KiB
TypeScript
241 lines
8.9 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import { Clock, Check, X, DollarSign, MessageCircle, User, Building } from 'lucide-react';
|
|
|
|
export interface PendingRequestDTO {
|
|
id: string;
|
|
sponsorId: string;
|
|
sponsorName: string;
|
|
sponsorLogo?: string;
|
|
tier: 'main' | 'secondary';
|
|
offeredAmount: number;
|
|
currency: string;
|
|
formattedAmount: string;
|
|
message?: string;
|
|
createdAt: Date;
|
|
platformFee: number;
|
|
netAmount: number;
|
|
}
|
|
|
|
interface PendingSponsorshipRequestsProps {
|
|
entityType: 'driver' | 'team' | 'race' | 'season';
|
|
entityId: string;
|
|
entityName: string;
|
|
requests: PendingRequestDTO[];
|
|
onAccept: (requestId: string) => Promise<void>;
|
|
onReject: (requestId: string, reason?: string) => Promise<void>;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
export default function PendingSponsorshipRequests({
|
|
entityType,
|
|
entityId,
|
|
entityName,
|
|
requests,
|
|
onAccept,
|
|
onReject,
|
|
isLoading = false,
|
|
}: PendingSponsorshipRequestsProps) {
|
|
const [processingId, setProcessingId] = useState<string | null>(null);
|
|
const [rejectModalId, setRejectModalId] = useState<string | null>(null);
|
|
const [rejectReason, setRejectReason] = useState('');
|
|
|
|
const handleAccept = async (requestId: string) => {
|
|
setProcessingId(requestId);
|
|
try {
|
|
await onAccept(requestId);
|
|
} finally {
|
|
setProcessingId(null);
|
|
}
|
|
};
|
|
|
|
const handleReject = async (requestId: string) => {
|
|
setProcessingId(requestId);
|
|
try {
|
|
await onReject(requestId, rejectReason || undefined);
|
|
setRejectModalId(null);
|
|
setRejectReason('');
|
|
} finally {
|
|
setProcessingId(null);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="text-center py-8 text-gray-400">
|
|
<div className="animate-pulse">Loading sponsorship requests...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (requests.length === 0) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<div className="w-12 h-12 mx-auto mb-3 rounded-full bg-iron-gray/50 flex items-center justify-center">
|
|
<Building className="w-6 h-6 text-gray-500" />
|
|
</div>
|
|
<p className="text-gray-400 text-sm">No pending sponsorship requests</p>
|
|
<p className="text-gray-500 text-xs mt-1">
|
|
When sponsors apply to sponsor this {entityType}, their requests will appear here. Sponsorships are attached to seasons, so you can change partners from season to season.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-white">Sponsorship Requests</h3>
|
|
<span className="px-2 py-1 text-xs bg-primary-blue/20 text-primary-blue rounded-full">
|
|
{requests.length} pending
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{requests.map((request) => {
|
|
const isProcessing = processingId === request.id;
|
|
const isRejecting = rejectModalId === request.id;
|
|
|
|
return (
|
|
<div
|
|
key={request.id}
|
|
className="rounded-lg border border-charcoal-outline bg-deep-graphite/70 p-4"
|
|
>
|
|
{/* Reject Modal */}
|
|
{isRejecting && (
|
|
<div className="mb-4 p-4 rounded-lg bg-iron-gray/50 border border-red-500/30">
|
|
<h4 className="text-sm font-medium text-white mb-2">
|
|
Reject sponsorship from {request.sponsorName}?
|
|
</h4>
|
|
<textarea
|
|
value={rejectReason}
|
|
onChange={(e) => setRejectReason(e.target.value)}
|
|
placeholder="Optional: Provide a reason for rejection..."
|
|
rows={2}
|
|
className="w-full rounded-lg border border-charcoal-outline bg-iron-gray/80 px-3 py-2 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-red-500 mb-3"
|
|
/>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => {
|
|
setRejectModalId(null);
|
|
setRejectReason('');
|
|
}}
|
|
className="px-3 py-1 text-xs"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => handleReject(request.id)}
|
|
disabled={isProcessing}
|
|
className="px-3 py-1 text-xs bg-red-600 hover:bg-red-700"
|
|
>
|
|
{isProcessing ? 'Rejecting...' : 'Confirm Reject'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex items-start gap-3 flex-1">
|
|
{/* Sponsor Logo */}
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-iron-gray/50 shrink-0">
|
|
{request.sponsorLogo ? (
|
|
<img
|
|
src={request.sponsorLogo}
|
|
alt={request.sponsorName}
|
|
className="w-8 h-8 object-contain"
|
|
/>
|
|
) : (
|
|
<Building className="w-6 h-6 text-gray-400" />
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h4 className="text-sm font-semibold text-white truncate">
|
|
{request.sponsorName}
|
|
</h4>
|
|
<span
|
|
className={`px-2 py-0.5 text-xs rounded-full ${
|
|
request.tier === 'main'
|
|
? 'bg-primary-blue/20 text-primary-blue'
|
|
: 'bg-gray-500/20 text-gray-400'
|
|
}`}
|
|
>
|
|
{request.tier === 'main' ? 'Main Sponsor' : 'Secondary'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Offer Details */}
|
|
<div className="flex flex-wrap gap-3 text-xs mb-2">
|
|
<div className="flex items-center gap-1 text-performance-green">
|
|
<DollarSign className="w-3 h-3" />
|
|
<span className="font-semibold">{request.formattedAmount}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 text-gray-500">
|
|
<span>Net: ${(request.netAmount / 100).toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 text-gray-500">
|
|
<Clock className="w-3 h-3" />
|
|
<span>
|
|
{new Date(request.createdAt).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Message */}
|
|
{request.message && (
|
|
<div className="flex items-start gap-1.5 text-xs text-gray-400 bg-iron-gray/30 rounded p-2">
|
|
<MessageCircle className="w-3 h-3 mt-0.5 shrink-0" />
|
|
<span className="line-clamp-2">{request.message}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{!isRejecting && (
|
|
<div className="flex gap-2 shrink-0">
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => handleAccept(request.id)}
|
|
disabled={isProcessing}
|
|
className="px-3 py-1.5 text-xs"
|
|
>
|
|
<Check className="w-3 h-3 mr-1" />
|
|
{isProcessing ? 'Accepting...' : 'Accept'}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setRejectModalId(request.id)}
|
|
disabled={isProcessing}
|
|
className="px-3 py-1.5 text-xs"
|
|
>
|
|
<X className="w-3 h-3 mr-1" />
|
|
Reject
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="text-xs text-gray-500 mt-4">
|
|
<p>
|
|
<strong className="text-gray-400">Note:</strong> Accepting a request will activate the sponsorship.
|
|
The sponsor will be charged per season and you'll receive the payment minus 10% platform fee.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |