website refactor

This commit is contained in:
2026-01-17 15:46:55 +01:00
parent 4d5ce9bfd6
commit 72a626ce71
346 changed files with 19308 additions and 8605 deletions

View File

@@ -1,251 +1,13 @@
'use client';
import { useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { StatCard } from '@/ui/StatCard';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { SectionHeader } from '@/ui/SectionHeader';
import { InfoBanner } from '@/ui/InfoBanner';
import { PageHeader } from '@/ui/PageHeader';
import { Icon } from '@/ui/Icon';
import { siteConfig } from '@/lib/siteConfig';
import { useSponsorBilling } from "@/hooks/sponsor/useSponsorBilling";
import {
CreditCard,
DollarSign,
Calendar,
Download,
Plus,
Check,
AlertTriangle,
FileText,
TrendingUp,
Receipt,
Building2,
Wallet,
Clock,
ChevronRight,
Info,
ExternalLink,
Percent,
Loader2
} from 'lucide-react';
import type { PaymentMethodDTO, InvoiceDTO } from '@/lib/types/tbd/SponsorBillingDTO';
// ============================================================================
// Components
// ============================================================================
function PaymentMethodCardComponent({
method,
onSetDefault,
onRemove
}: {
method: PaymentMethodDTO;
onSetDefault: () => void;
onRemove: () => void;
}) {
const shouldReduceMotion = useReducedMotion();
const getIcon = () => {
if (method.type === 'sepa') return Building2;
return CreditCard;
};
const MethodIcon = getIcon();
const displayLabel = method.type === 'sepa' && method.bankName
? `${method.bankName} •••• ${method.last4}`
: `${method.brand} •••• ${method.last4}`;
const expiryDisplay = method.expiryMonth && method.expiryYear
? `${method.expiryMonth}/${method.expiryYear}`
: null;
return (
<Box
as={motion.div}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}
p={4}
rounded="xl"
border
borderColor={method.isDefault ? 'border-primary-blue/50' : 'border-charcoal-outline'}
bg={method.isDefault ? 'bg-gradient-to-r from-primary-blue/10 to-transparent' : 'bg-iron-gray/30'}
shadow={method.isDefault ? '0_0_20px_rgba(25,140,255,0.1)' : undefined}
hoverBorderColor={!method.isDefault ? 'border-charcoal-outline/80' : undefined}
transition-all
>
<Box display="flex" alignItems="center" justifyContent="between">
<Box display="flex" alignItems="center" gap={4}>
<Box
w="12"
h="12"
rounded="xl"
display="flex"
alignItems="center"
justifyContent="center"
bg={method.isDefault ? 'bg-primary-blue/20' : 'bg-iron-gray'}
>
<Icon icon={MethodIcon} size={6} color={method.isDefault ? 'text-primary-blue' : 'text-gray-400'} />
</Box>
<Box>
<Box display="flex" alignItems="center" gap={2}>
<Text weight="medium" color="text-white">{displayLabel}</Text>
{method.isDefault && (
<Box px={2} py={0.5} rounded="full" bg="bg-primary-blue/20">
<Text size="xs" color="text-primary-blue" weight="medium">
Default
</Text>
</Box>
)}
</Box>
{expiryDisplay && (
<Text size="sm" color="text-gray-500" block>
Expires {expiryDisplay}
</Text>
)}
{method.type === 'sepa' && (
<Text size="sm" color="text-gray-500" block>SEPA Direct Debit</Text>
)}
</Box>
</Box>
<Box display="flex" alignItems="center" gap={2}>
{!method.isDefault && (
<Button variant="secondary" onClick={onSetDefault} size="sm">
Set Default
</Button>
)}
<Button variant="secondary" onClick={onRemove} size="sm" color="text-gray-400" hoverTextColor="text-racing-red">
Remove
</Button>
</Box>
</Box>
</Box>
);
}
function InvoiceRowComponent({ invoice, index }: { invoice: InvoiceDTO; index: number }) {
const shouldReduceMotion = useReducedMotion();
const statusConfig = {
paid: {
icon: Check,
label: 'Paid',
color: 'text-performance-green',
bg: 'bg-performance-green/10',
border: 'border-performance-green/30'
},
pending: {
icon: Clock,
label: 'Pending',
color: 'text-warning-amber',
bg: 'bg-warning-amber/10',
border: 'border-warning-amber/30'
},
overdue: {
icon: AlertTriangle,
label: 'Overdue',
color: 'text-racing-red',
bg: 'bg-racing-red/10',
border: 'border-racing-red/30'
},
failed: {
icon: AlertTriangle,
label: 'Failed',
color: 'text-racing-red',
bg: 'bg-racing-red/10',
border: 'border-racing-red/30'
},
};
const typeLabels = {
league: 'League',
team: 'Team',
driver: 'Driver',
race: 'Race',
platform: 'Platform',
};
const status = statusConfig[invoice.status as keyof typeof statusConfig];
const StatusIcon = status.icon;
return (
<Box
as={motion.div}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.2, delay: shouldReduceMotion ? 0 : index * 0.05 }}
display="flex"
alignItems="center"
justifyContent="between"
p={4}
borderBottom
borderColor="border-charcoal-outline/50"
hoverBg="bg-iron-gray/20"
transition-colors
group
>
<Box display="flex" alignItems="center" gap={4} flexGrow={1}>
<Box w="10" h="10" rounded="lg" bg="bg-iron-gray" display="flex" alignItems="center" justifyContent="center">
<Icon icon={Receipt} size={5} color="text-gray-400" />
</Box>
<Box flexGrow={1} minWidth="0">
<Box display="flex" alignItems="center" gap={2} mb={0.5}>
<Text weight="medium" color="text-white" truncate>{invoice.description}</Text>
<Box px={2} py={0.5} rounded bg="bg-iron-gray">
<Text size="xs" color="text-gray-400">
{typeLabels[invoice.sponsorshipType as keyof typeof typeLabels]}
</Text>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={3}>
<Text size="sm" color="text-gray-500">{invoice.invoiceNumber}</Text>
<Text size="sm" color="text-gray-500"></Text>
<Text size="sm" color="text-gray-500">
{new globalThis.Date(invoice.date).toLocaleDateString()}
</Text>
</Box>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={6}>
<Box textAlign="right">
<Text weight="semibold" color="text-white" block>
${invoice.totalAmount.toFixed(2)}
</Text>
<Text size="xs" color="text-gray-500" block>
incl. ${invoice.vatAmount.toFixed(2)} VAT
</Text>
</Box>
<Box display="flex" alignItems="center" gap={1.5} px={2.5} py={1} rounded="full" bg={status.bg} border borderColor={status.border}>
<Icon icon={StatusIcon} size={3} color={status.color} />
<Text size="xs" weight="medium" color={status.color}>{status.label}</Text>
</Box>
<Button variant="secondary" size="sm" opacity={0} groupHoverTextColor="opacity-100" transition-opacity icon={<Icon icon={Download} size={3} />}>
PDF
</Button>
</Box>
</Box>
);
}
// ============================================================================
// Main Component
// ============================================================================
import { SponsorBillingTemplate } from "@/templates/SponsorBillingTemplate";
import { Box } from "@/ui/Box";
import { Text } from "@/ui/Text";
import { Button } from "@/ui/Button";
import { DollarSign, AlertTriangle, Calendar, TrendingUp } from "lucide-react";
export default function SponsorBillingPage() {
const shouldReduceMotion = useReducedMotion();
const [showAllInvoices, setShowAllInvoices] = useState(false);
const { data: billingData, isLoading, error, retry } = useSponsorBilling('demo-sponsor-1');
if (isLoading) {
@@ -274,228 +36,68 @@ export default function SponsorBillingPage() {
);
}
const data = billingData;
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 (window.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 },
visible: {
opacity: 1,
transition: {
staggerChildren: shouldReduceMotion ? 0 : 0.1,
},
},
const handleDownloadInvoice = (invoiceId: string) => {
console.log('Downloading invoice:', invoiceId);
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
const billingStats = [
{
label: 'Total Spent',
value: `$${billingData.stats.totalSpent.toFixed(2)}`,
subValue: 'All time',
icon: DollarSign,
variant: 'success' as const,
},
{
label: 'Pending Payments',
value: `$${billingData.stats.pendingAmount.toFixed(2)}`,
subValue: `${billingData.invoices.filter(i => i.status === 'pending' || i.status === 'overdue').length} invoices`,
icon: AlertTriangle,
variant: (billingData.stats.pendingAmount > 0 ? 'warning' : 'default') as 'warning' | 'default',
},
{
label: 'Next Payment',
value: new Date(billingData.stats.nextPaymentDate).toLocaleDateString(),
subValue: `$${billingData.stats.nextPaymentAmount.toFixed(2)}`,
icon: Calendar,
variant: 'info' as const,
},
{
label: 'Monthly Average',
value: `$${billingData.stats.averageMonthlySpend.toFixed(2)}`,
subValue: 'Last 6 months',
icon: TrendingUp,
variant: 'default' as const,
},
];
const transactions = billingData.invoices.map(inv => ({
id: inv.id,
date: inv.date,
description: inv.description,
amount: inv.totalAmount,
status: inv.status as any,
invoiceNumber: inv.invoiceNumber,
type: inv.sponsorshipType,
}));
return (
<Box
maxWidth="5xl"
mx="auto"
py={8}
px={4}
as={motion.div}
// @ts-ignore
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Header */}
<Box as={motion.div} variants={itemVariants}>
<PageHeader
icon={Wallet}
title="Billing & Payments"
description="Manage payment methods, view invoices, and track your sponsorship spending"
iconGradient="from-warning-amber/20 to-warning-amber/5"
iconBorder="border-warning-amber/30"
/>
</Box>
{/* Stats Grid */}
<Box as={motion.div} variants={itemVariants} display="grid" gridCols={{ base: 1, md: 2, lg: 4 }} gap={4} mb={8}>
<StatCard
icon={DollarSign}
label="Total Spent"
value={`$${data.stats.totalSpent.toFixed(2)}`}
subValue="All time"
variant="green"
/>
<StatCard
icon={AlertTriangle}
label="Pending Payments"
value={`$${data.stats.pendingAmount.toFixed(2)}`}
subValue={`${data.invoices.filter((i: { status: string }) => i.status === 'pending' || i.status === 'overdue').length} invoices`}
variant="orange"
/>
<StatCard
icon={Calendar}
label="Next Payment"
value={new globalThis.Date(data.stats.nextPaymentDate).toLocaleDateString()}
subValue={`$${data.stats.nextPaymentAmount.toFixed(2)}`}
variant="blue"
/>
<StatCard
icon={TrendingUp}
label="Monthly Average"
value={`$${data.stats.averageMonthlySpend.toFixed(2)}`}
subValue="Last 6 months"
variant="blue"
/>
</Box>
{/* Payment Methods */}
<Box as={motion.div} variants={itemVariants}>
<Card mb={8} overflow="hidden">
<SectionHeader
icon={CreditCard}
title="Payment Methods"
action={
<Button variant="secondary" size="sm" icon={<Icon icon={Plus} size={4} />}>
Add Payment Method
</Button>
}
/>
<Box p={5} display="flex" flexDirection="col" gap={3}>
{data.paymentMethods.map((method: PaymentMethodDTO) => (
<PaymentMethodCardComponent
key={method.id}
method={method}
onSetDefault={() => handleSetDefault(method.id)}
onRemove={() => handleRemoveMethod(method.id)}
/>
))}
</Box>
<Box px={5} pb={5}>
<InfoBanner type="info">
<Text block mb={1}>We support Visa, Mastercard, American Express, and SEPA Direct Debit.</Text>
<Text block>All payment information is securely processed and stored by our payment provider.</Text>
</InfoBanner>
</Box>
</Card>
</Box>
{/* Billing History */}
<Box as={motion.div} variants={itemVariants}>
<Card mb={8} overflow="hidden">
<SectionHeader
icon={FileText}
title="Billing History"
color="text-warning-amber"
action={
<Button variant="secondary" size="sm" icon={<Icon icon={Download} size={4} />}>
Export All
</Button>
}
/>
<Box>
{data.invoices.slice(0, showAllInvoices ? data.invoices.length : 4).map((invoice: InvoiceDTO, index: number) => (
<InvoiceRowComponent key={invoice.id} invoice={invoice} index={index} />
))}
</Box>
{data.invoices.length > 4 && (
<Box p={4} borderTop borderColor="border-charcoal-outline">
<Button
variant="secondary"
fullWidth
onClick={() => setShowAllInvoices(!showAllInvoices)}
icon={<Icon icon={ChevronRight} size={4} className={showAllInvoices ? 'rotate-90' : ''} />}
flexDirection="row-reverse"
>
{showAllInvoices ? 'Show Less' : `View All ${data.invoices.length} Invoices`}
</Button>
</Box>
)}
</Card>
</Box>
{/* Platform Fee & VAT Information */}
<Box as={motion.div} variants={itemVariants} display="grid" gridCols={{ base: 1, md: 2 }} gap={6}>
{/* Platform Fee */}
<Card overflow="hidden">
<Box p={5} borderBottom borderColor="border-charcoal-outline" bg="bg-gradient-to-r from-iron-gray/30 to-transparent">
<Heading level={3} fontSize="base" weight="semibold" color="text-white" icon={<Box p={2} rounded="lg" bg="bg-iron-gray/50"><Icon icon={Percent} size={4} color="text-primary-blue" /></Box>}>
Platform Fee
</Heading>
</Box>
<Box p={5}>
<Text size="3xl" weight="bold" color="text-white" block mb={2}>
{siteConfig.fees.platformFeePercent}%
</Text>
<Text size="sm" color="text-gray-400" block mb={4}>
{siteConfig.fees.description}
</Text>
<Box display="flex" flexDirection="col" gap={1}>
<Text size="xs" color="text-gray-500" block> Applied to all sponsorship payments</Text>
<Text size="xs" color="text-gray-500" block> Covers platform maintenance and analytics</Text>
<Text size="xs" color="text-gray-500" block> Ensures quality sponsorship placements</Text>
</Box>
</Box>
</Card>
{/* VAT Information */}
<Card overflow="hidden">
<Box p={5} borderBottom borderColor="border-charcoal-outline" bg="bg-gradient-to-r from-iron-gray/30 to-transparent">
<Heading level={3} fontSize="base" weight="semibold" color="text-white" icon={<Box p={2} rounded="lg" bg="bg-iron-gray/50"><Icon icon={Receipt} size={4} color="text-performance-green" /></Box>}>
VAT Information
</Heading>
</Box>
<Box p={5}>
<Text size="sm" color="text-gray-400" block mb={4}>
{siteConfig.vat.notice}
</Text>
<Box display="flex" flexDirection="col" gap={3}>
<Box display="flex" justifyContent="between" alignItems="center" py={2} borderBottom borderColor="border-charcoal-outline/50">
<Text color="text-gray-500">Standard VAT Rate</Text>
<Text color="text-white" weight="medium">{siteConfig.vat.standardRate}%</Text>
</Box>
<Box display="flex" justifyContent="between" alignItems="center" py={2}>
<Text color="text-gray-500">B2B Reverse Charge</Text>
<Text color="text-performance-green" weight="medium">Available</Text>
</Box>
</Box>
<Text size="xs" color="text-gray-500" block mt={4}>
Enter your VAT ID in Settings to enable reverse charge for B2B transactions.
</Text>
</Box>
</Card>
</Box>
{/* Billing Support */}
<Box as={motion.div} variants={itemVariants} mt={6}>
<Card p={5}>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" gap={4}>
<Box p={3} rounded="xl" bg="bg-iron-gray">
<Icon icon={Info} size={5} color="text-gray-400" />
</Box>
<Box>
<Heading level={3} fontSize="base" weight="medium" color="text-white">Need help with billing?</Heading>
<Text size="sm" color="text-gray-500" block>
Contact our billing support for questions about invoices, payments, or refunds.
</Text>
</Box>
</Stack>
<Button variant="secondary" icon={<Icon icon={ExternalLink} size={4} />}>
Contact Support
</Button>
</Box>
</Card>
</Box>
</Box>
<SponsorBillingTemplate
viewData={billingData}
billingStats={billingStats}
transactions={transactions}
onSetDefaultPaymentMethod={handleSetDefault}
onDownloadInvoice={handleDownloadInvoice}
/>
);
}

View File

@@ -1,619 +1,74 @@
'use client';
import { useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { motion, useReducedMotion } from 'framer-motion';
import Link from 'next/link';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { InfoBanner } from '@/ui/InfoBanner';
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
import {
Megaphone,
Trophy,
Users,
Eye,
Calendar,
ExternalLink,
Plus,
ChevronRight,
Check,
Clock,
XCircle,
Car,
Flag,
Search,
TrendingUp,
BarChart3,
ArrowUpRight,
ArrowDownRight,
Send,
ThumbsUp,
ThumbsDown,
RefreshCw,
} from 'lucide-react';
// ============================================================================
// Types
// ============================================================================
type SponsorshipType = 'all' | 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
type SponsorshipStatus = 'all' | 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
// ============================================================================
// Configuration
// ============================================================================
const TYPE_CONFIG = {
leagues: { icon: Trophy, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', label: 'League' },
teams: { icon: Users, color: 'text-purple-400', bgColor: 'bg-purple-400/10', label: 'Team' },
drivers: { icon: Car, color: 'text-performance-green', bgColor: 'bg-performance-green/10', label: 'Driver' },
races: { icon: Flag, color: 'text-warning-amber', bgColor: 'bg-warning-amber/10', label: 'Race' },
platform: { icon: Megaphone, color: 'text-racing-red', bgColor: 'bg-racing-red/10', label: 'Platform' },
all: { icon: BarChart3, color: 'text-gray-400', bgColor: 'bg-gray-400/10', label: 'All' },
};
const STATUS_CONFIG = {
active: {
icon: Check,
color: 'text-performance-green',
bgColor: 'bg-performance-green/10',
borderColor: 'border-performance-green/30',
label: 'Active'
},
pending_approval: {
icon: Clock,
color: 'text-warning-amber',
bgColor: 'bg-warning-amber/10',
borderColor: 'border-warning-amber/30',
label: 'Awaiting Approval'
},
approved: {
icon: ThumbsUp,
color: 'text-primary-blue',
bgColor: 'bg-primary-blue/10',
borderColor: 'border-primary-blue/30',
label: 'Approved'
},
rejected: {
icon: ThumbsDown,
color: 'text-racing-red',
bgColor: 'bg-racing-red/10',
borderColor: 'border-racing-red/30',
label: 'Declined'
},
expired: {
icon: XCircle,
color: 'text-gray-400',
bgColor: 'bg-gray-400/10',
borderColor: 'border-gray-400/30',
label: 'Expired'
},
};
// ============================================================================
// Components
// ============================================================================
function SponsorshipCard({ sponsorship }: { sponsorship: unknown }) {
const shouldReduceMotion = useReducedMotion();
const s = sponsorship as any; // Temporary cast to avoid breaking logic
const typeConfig = TYPE_CONFIG[s.type as keyof typeof TYPE_CONFIG];
const statusConfig = STATUS_CONFIG[s.status as keyof typeof STATUS_CONFIG];
const TypeIcon = typeConfig.icon;
const StatusIcon = statusConfig.icon;
const daysRemaining = Math.ceil((s.endDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
const isExpiringSoon = daysRemaining > 0 && daysRemaining <= 30;
const isPending = s.status === 'pending_approval';
const isRejected = s.status === 'rejected';
const isApproved = s.status === 'approved';
const getEntityLink = () => {
switch (s.type) {
case 'leagues': return `/leagues/${s.entityId}`;
case 'teams': return `/teams/${s.entityId}`;
case 'drivers': return `/drivers/${s.entityId}`;
case 'races': return `/races/${s.entityId}`;
default: return '#';
}
};
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
whileHover={{ y: -2 }}
transition={{ duration: 0.2 }}
>
<Card className={`hover:border-primary-blue/30 transition-all duration-300 ${
isPending ? 'border-warning-amber/30' :
isRejected ? 'border-racing-red/20 opacity-75' :
isApproved ? 'border-primary-blue/30' : ''
}`}>
{/* Header */}
<Stack direction="row" align="start" justify="between" mb={4}>
<Stack direction="row" align="center" gap={3}>
<Box w="10" h="10" rounded="lg" bg={typeConfig.bgColor} display="flex" alignItems="center" justifyContent="center">
<TypeIcon className={`w-5 h-5 ${typeConfig.color}`} />
</Box>
<Box>
<Box display="flex" alignItems="center" gap={2} flexWrap="wrap">
<Text size="xs" weight="medium" px={2} py={0.5} rounded="md" bg={typeConfig.bgColor} color={typeConfig.color}>
{typeConfig.label}
</Text>
{s.tier && (
<Text size="xs" weight="medium" px={2} py={0.5} rounded="md" bg={s.tier === 'main' ? 'bg-primary-blue/20' : 'bg-purple-400/20'} color={s.tier === 'main' ? 'text-primary-blue' : 'text-purple-400'}>
{s.tier === 'main' ? 'Main Sponsor' : 'Secondary'}
</Text>
)}
</Box>
</Box>
</Stack>
<Box display="flex" alignItems="center" gap={1} px={2.5} py={1} rounded="full" border bg={statusConfig.bgColor} color={statusConfig.color} borderColor={statusConfig.borderColor}>
<StatusIcon className="w-3 h-3" />
<Text size="xs" weight="medium">{statusConfig.label}</Text>
</Box>
</Stack>
{/* Entity Name */}
<Heading level={3} fontSize="lg" weight="semibold" color="text-white" mb={1}>{s.entityName}</Heading>
{s.details && (
<Text size="sm" color="text-gray-500" block mb={3}>{s.details}</Text>
)}
{/* Application/Approval Info for non-active states */}
{isPending && (
<Box mb={4} p={3} rounded="lg" bg="bg-warning-amber/5" border borderColor="border-warning-amber/20">
<Stack direction="row" align="center" gap={2} color="text-warning-amber" mb={2}>
<Send className="w-4 h-4" />
<Text size="sm" weight="medium">Application Pending</Text>
</Stack>
<Text size="xs" color="text-gray-400" block mb={2}>
Sent to <Text color="text-gray-300">{s.entityOwner}</Text> on{' '}
{s.applicationDate?.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</Text>
{s.applicationMessage && (
<Text size="xs" color="text-gray-500" italic block>&quot;{s.applicationMessage}&quot;</Text>
)}
</Box>
)}
{isApproved && (
<Box mb={4} p={3} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
<Stack direction="row" align="center" gap={2} color="text-primary-blue" mb={1}>
<ThumbsUp className="w-4 h-4" />
<Text size="sm" weight="medium">Approved!</Text>
</Stack>
<Text size="xs" color="text-gray-400" block>
Approved by <Text color="text-gray-300">{s.entityOwner}</Text> on{' '}
{s.approvalDate?.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</Text>
<Text size="xs" color="text-gray-500" block mt={1}>
Starts {s.startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</Text>
</Box>
)}
{isRejected && (
<Box mb={4} p={3} rounded="lg" bg="bg-racing-red/5" border borderColor="border-racing-red/20">
<Stack direction="row" align="center" gap={2} color="text-racing-red" mb={1}>
<ThumbsDown className="w-4 h-4" />
<Text size="sm" weight="medium">Application Declined</Text>
</Stack>
{s.rejectionReason && (
<Text size="xs" color="text-gray-400" block mt={1}>
Reason: <Text color="text-gray-300">{s.rejectionReason}</Text>
</Text>
)}
<Button variant="secondary" className="mt-2 text-xs">
<RefreshCw className="w-3 h-3 mr-1" />
Reapply
</Button>
</Box>
)}
{/* Metrics Grid - Only show for active sponsorships */}
{s.status === 'active' && (
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={3} mb={4}>
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
<Eye className="w-3 h-3" />
<Text size="xs">Impressions</Text>
</Box>
<Stack direction="row" align="center" gap={2}>
<Text color="text-white" weight="semibold">{s.formattedImpressions}</Text>
{s.impressionsChange !== undefined && s.impressionsChange !== 0 && (
<Text size="xs" display="flex" alignItems="center" color={s.impressionsChange > 0 ? 'text-performance-green' : 'text-racing-red'}>
{s.impressionsChange > 0 ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
{Math.abs(s.impressionsChange)}%
</Text>
)}
</Stack>
</Box>
{s.engagement && (
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
<TrendingUp className="w-3 h-3" />
<Text size="xs">Engagement</Text>
</Box>
<Text color="text-white" weight="semibold">{s.engagement}%</Text>
</Box>
)}
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
<Calendar className="w-3 h-3" />
<Text size="xs">Period</Text>
</Box>
<Text color="text-white" weight="semibold" size="xs">
{s.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} - {s.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Text>
</Box>
<Box bg="bg-iron-gray/50" rounded="lg" p={3}>
<Box display="flex" alignItems="center" gap={1} color="text-gray-400" mb={1}>
<Trophy className="w-3 h-3" />
<Text size="xs">Investment</Text>
</Box>
<Text color="text-white" weight="semibold">{s.formattedPrice}</Text>
</Box>
</Box>
)}
{/* Basic info for non-active */}
{s.status !== 'active' && (
<Stack direction="row" align="center" gap={4} mb={4}>
<Box display="flex" alignItems="center" gap={1} color="text-gray-400">
<Calendar className="w-3.5 h-3.5" />
<Text size="sm">{s.periodDisplay}</Text>
</Box>
<Box display="flex" alignItems="center" gap={1} color="text-gray-400">
<Trophy className="w-3.5 h-3.5" />
<Text size="sm">{s.formattedPrice}</Text>
</Box>
</Stack>
)}
{/* Footer */}
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-charcoal-outline/50">
<Box display="flex" alignItems="center" gap={2}>
{s.status === 'active' && (
<Text size="xs" color={isExpiringSoon ? 'text-warning-amber' : 'text-gray-500'}>
{daysRemaining > 0 ? `${daysRemaining} days remaining` : 'Ended'}
</Text>
)}
{isPending && (
<Text size="xs" color="text-gray-500">
Waiting for response...
</Text>
)}
</Box>
<Stack direction="row" align="center" gap={2}>
{s.type !== 'platform' && (
<Link href={getEntityLink()}>
<Button variant="secondary" className="text-xs">
<ExternalLink className="w-3 h-3 mr-1" />
View
</Button>
</Link>
)}
{isPending && (
<Button variant="secondary" className="text-xs text-racing-red hover:bg-racing-red/10">
Cancel Application
</Button>
)}
{s.status === 'active' && (
<Button variant="secondary" className="text-xs">
Details
<ChevronRight className="w-3 h-3 ml-1" />
</Button>
)}
</Stack>
</Box>
</Card>
</motion.div>
);
}
// ============================================================================
// Main Component
// ============================================================================
import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate";
import { Box } from "@/ui/Box";
import { Text } from "@/ui/Text";
import { Button } from "@/ui/Button";
export default function SponsorCampaignsPage() {
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 [typeFilter, setTypeFilter] = useState<SponsorshipType>('all');
const [searchQuery, setSearchQuery] = useState('');
const { data: sponsorshipsData, isLoading, error, retry } = useSponsorSponsorships('demo-sponsor-1');
if (isLoading) {
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>
<Box maxWidth="7xl" mx="auto" py={8} px={4} display="flex" alignItems="center" justifyContent="center" minHeight="400px">
<Box textAlign="center">
<Box w="8" h="8" border borderTop={false} borderColor="border-primary-blue" rounded="full" animate="spin" mx="auto" mb={4} />
<Text color="text-gray-400">Loading sponsorships...</Text>
</Box>
</Box>
);
}
if (error || !sponsorshipsData) {
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?.getUserMessage() || 'No sponsorships data available'}</p>
<Box maxWidth="7xl" mx="auto" py={8} px={4} display="flex" alignItems="center" justifyContent="center" minHeight="400px">
<Box textAlign="center">
<Text color="text-gray-400">{error?.getUserMessage() || 'No sponsorships data available'}</Text>
{error && (
<Button variant="secondary" onClick={retry} className="mt-4">
<Button variant="secondary" onClick={retry} mt={4}>
Retry
</Button>
)}
</div>
</div>
</Box>
</Box>
);
}
const data = sponsorshipsData;
// Calculate stats
const stats = {
total: sponsorshipsData.sponsorships.length,
active: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').length,
pending: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'pending_approval').length,
approved: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'approved').length,
rejected: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'rejected').length,
totalInvestment: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0),
totalImpressions: sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0),
};
// Filter sponsorships
const filteredSponsorships = data.sponsorships.filter((s: unknown) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sponsorship = s as any;
if (typeFilter !== 'all' && sponsorship.type !== typeFilter) return false;
if (statusFilter !== 'all' && sponsorship.status !== statusFilter) return false;
if (searchQuery && !sponsorship.entityName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
const viewData = {
sponsorships: sponsorshipsData.sponsorships as any,
stats,
};
const filteredSponsorships = sponsorshipsData.sponsorships.filter((s: any) => {
// For now, we only have leagues in the DTO
if (typeFilter !== 'all' && typeFilter !== 'leagues') return false;
if (searchQuery && !s.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
return true;
});
// Calculate stats
const stats = {
total: data.sponsorships.length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
active: data.sponsorships.filter((s: any) => s.status === 'active').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pending: data.sponsorships.filter((s: any) => s.status === 'pending_approval').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
approved: data.sponsorships.filter((s: any) => s.status === 'approved').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rejected: data.sponsorships.filter((s: any) => s.status === 'rejected').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
totalInvestment: data.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
totalImpressions: data.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0),
};
// Stats by type
const statsByType = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leagues: data.sponsorships.filter((s: any) => s.type === 'leagues').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
teams: data.sponsorships.filter((s: any) => s.type === 'teams').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
drivers: data.sponsorships.filter((s: any) => s.type === 'drivers').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
races: data.sponsorships.filter((s: any) => s.type === 'races').length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
platform: data.sponsorships.filter((s: any) => s.type === 'platform').length,
};
return (
<Box maxWidth="7xl" mx="auto" py={8} px={4}>
{/* Header */}
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4} mb={8}>
<Box>
<Heading level={1} fontSize="2xl" weight="bold" color="text-white" icon={<Megaphone className="w-7 h-7 text-primary-blue" />}>
My Sponsorships
</Heading>
<Text color="text-gray-400" mt={1} block>Manage applications and active sponsorship campaigns</Text>
</Box>
<Box display="flex" alignItems="center" gap={3}>
<Link href="/leagues">
<Button variant="primary">
<Plus className="w-4 h-4 mr-2" />
Find Opportunities
</Button>
</Link>
</Box>
</Stack>
{/* Info Banner about how sponsorships work */}
{stats.pending > 0 && (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6"
>
<InfoBanner type="info" title="Sponsorship Applications">
<Text size="sm">
You have <Text weight="bold" color="text-white">{stats.pending} pending application{stats.pending !== 1 ? 's' : ''}</Text> waiting for approval.
League admins, team owners, and drivers review applications before accepting sponsorships.
</Text>
</InfoBanner>
</motion.div>
)}
{/* Quick Stats */}
<Box display="grid" gridCols={{ base: 2, md: 6 }} gap={4} mb={8}>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="p-4">
<Text size="2xl" weight="bold" color="text-white" block>{stats.total}</Text>
<Text size="sm" color="text-gray-400">Total</Text>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
>
<Card className="p-4">
<Text size="2xl" weight="bold" color="text-performance-green" block>{stats.active}</Text>
<Text size="sm" color="text-gray-400">Active</Text>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className={`p-4 ${stats.pending > 0 ? 'border-warning-amber/30' : ''}`}>
<Text size="2xl" weight="bold" color="text-warning-amber" block>{stats.pending}</Text>
<Text size="sm" color="text-gray-400">Pending</Text>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
>
<Card className="p-4">
<Text size="2xl" weight="bold" color="text-primary-blue" block>{stats.approved}</Text>
<Text size="sm" color="text-gray-400">Approved</Text>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="p-4">
<Text size="2xl" weight="bold" color="text-white" block>${stats.totalInvestment.toLocaleString()}</Text>
<Text size="sm" color="text-gray-400">Active Investment</Text>
</Card>
</motion.div>
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.25 }}
>
<Card className="p-4">
<Text size="2xl" weight="bold" color="text-primary-blue" block>{(stats.totalImpressions / 1000).toFixed(0)}k</Text>
<Text size="sm" color="text-gray-400">Impressions</Text>
</Card>
</motion.div>
</Box>
{/* Filters */}
<Stack direction={{ base: 'col', lg: 'row' }} gap={4} mb={6}>
{/* Search */}
<Box position="relative" flexGrow={1}>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="text"
placeholder="Search sponsorships..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-charcoal-outline bg-iron-gray text-white placeholder-gray-500 focus:border-primary-blue focus:outline-none"
/>
</Box>
{/* Type Filter */}
<Box display="flex" alignItems="center" gap={2} overflow="auto" pb={{ base: 2, lg: 0 }}>
{(['all', 'leagues', 'teams', 'drivers', 'races', 'platform'] as const).map((type) => {
const config = TYPE_CONFIG[type];
const Icon = config.icon;
const count = type === 'all' ? stats.total : statsByType[type];
return (
<button
key={type}
onClick={() => setTypeFilter(type)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors ${
typeFilter === type
? 'bg-primary-blue text-white'
: 'bg-iron-gray/50 text-gray-400 hover:bg-iron-gray'
} border-0 cursor-pointer`}
>
<Icon className="w-4 h-4" />
{config.label}
<Text size="xs" px={1.5} py={0.5} rounded="sm" bg={typeFilter === type ? 'bg-white/20' : 'bg-charcoal-outline'}>
{count}
</Text>
</button>
);
})}
</Box>
{/* Status Filter */}
<Box display="flex" alignItems="center" gap={2} overflow="auto">
{(['all', 'active', 'pending_approval', 'approved', 'rejected'] as const).map((status) => {
const config = status === 'all'
? { label: 'All', color: 'text-gray-400' }
: STATUS_CONFIG[status];
const count = status === 'all'
? stats.total
: data.sponsorships.filter((s: any) => s.status === status).length;
return (
<button
key={status}
onClick={() => setStatusFilter(status)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
statusFilter === status
? 'bg-iron-gray text-white border border-charcoal-outline'
: 'text-gray-500 hover:text-gray-300'
} border-0 cursor-pointer`}
>
{config.label}
{count > 0 && status !== 'all' && (
<Text size="xs" ml={1.5} px={1.5} py={0.5} rounded="sm" bg={status === 'pending_approval' ? 'bg-warning-amber/20' : 'bg-charcoal-outline'} color={status === 'pending_approval' ? 'text-warning-amber' : ''}>
{count}
</Text>
)}
</button>
);
})}
</Box>
</Stack>
{/* Sponsorship List */}
{filteredSponsorships.length === 0 ? (
<Card className="text-center py-16">
<Megaphone className="w-12 h-12 text-gray-600 mx-auto mb-4" />
<Heading level={3} fontSize="lg" weight="semibold" color="text-white" mb={2}>No sponsorships found</Heading>
<Text color="text-gray-400" mb={6} maxWidth="md" mx="auto" block>
{searchQuery || typeFilter !== 'all' || statusFilter !== 'all'
? 'Try adjusting your filters to see more results.'
: 'Start sponsoring leagues, teams, or drivers to grow your brand visibility.'}
</Text>
<Stack direction={{ base: 'col', md: 'row' }} gap={3} justify="center">
<Link href="/leagues">
<Button variant="primary">
<Trophy className="w-4 h-4 mr-2" />
Browse Leagues
</Button>
</Link>
<Link href="/teams">
<Button variant="secondary">
<Users className="w-4 h-4 mr-2" />
Browse Teams
</Button>
</Link>
<Link href="/drivers">
<Button variant="secondary">
<Car className="w-4 h-4 mr-2" />
Browse Drivers
</Button>
</Link>
</Stack>
</Card>
) : (
<Box display="grid" gridCols={{ base: 1, lg: 2 }} gap={4}>
{filteredSponsorships.map((sponsorship: any) => (
<SponsorshipCard key={sponsorship.id} sponsorship={sponsorship} />
))}
</Box>
)}
</Box>
<SponsorCampaignsTemplate
viewData={viewData}
filteredSponsorships={filteredSponsorships as any}
typeFilter={typeFilter}
setTypeFilter={setTypeFilter}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
);
}

View File

@@ -1,92 +1,16 @@
'use client';
import { useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { Toggle } from '@/ui/Toggle';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { SectionHeader } from '@/ui/SectionHeader';
import { FormField } from '@/ui/FormField';
import { PageHeader } from '@/ui/PageHeader';
import { Image } from '@/ui/Image';
import {
Settings,
Building2,
Mail,
Globe,
Upload,
Save,
Bell,
Shield,
Eye,
Trash2,
CheckCircle,
User,
Phone,
MapPin,
FileText,
Link as LinkIcon,
Image as ImageIcon,
Lock,
Key,
Smartphone,
AlertCircle
} from 'lucide-react';
import { SponsorSettingsTemplate } from '@/templates/SponsorSettingsTemplate';
import { logoutAction } from '@/app/actions/logoutAction';
// ============================================================================
// Types
// ============================================================================
interface SponsorProfile {
companyName: string;
contactName: string;
contactEmail: string;
contactPhone: string;
website: string;
description: string;
logoUrl: string | null;
industry: string;
address: {
street: string;
city: string;
country: string;
postalCode: string;
};
taxId: string;
socialLinks: {
twitter: string;
linkedin: string;
instagram: string;
};
}
interface NotificationSettings {
emailNewSponsorships: boolean;
emailWeeklyReport: boolean;
emailRaceAlerts: boolean;
emailPaymentAlerts: boolean;
emailNewOpportunities: boolean;
emailContractExpiry: boolean;
}
interface PrivacySettings {
publicProfile: boolean;
showStats: boolean;
showActiveSponsorships: boolean;
allowDirectContact: boolean;
}
import { ConfirmDialog } from '@/components/shared/ux/ConfirmDialog';
import { useRouter } from 'next/navigation';
// ============================================================================
// Mock Data
// ============================================================================
const MOCK_PROFILE: SponsorProfile = {
const MOCK_PROFILE = {
companyName: 'Acme Racing Co.',
contactName: 'John Smith',
contactEmail: 'sponsor@acme-racing.com',
@@ -109,7 +33,7 @@ const MOCK_PROFILE: SponsorProfile = {
},
};
const MOCK_NOTIFICATIONS: NotificationSettings = {
const MOCK_NOTIFICATIONS = {
emailNewSponsorships: true,
emailWeeklyReport: true,
emailRaceAlerts: false,
@@ -118,581 +42,71 @@ const MOCK_NOTIFICATIONS: NotificationSettings = {
emailContractExpiry: true,
};
const MOCK_PRIVACY: PrivacySettings = {
const MOCK_PRIVACY = {
publicProfile: true,
showStats: false,
showActiveSponsorships: true,
allowDirectContact: true,
};
const INDUSTRY_OPTIONS = [
'Racing Equipment',
'Automotive',
'Technology',
'Gaming & Esports',
'Energy Drinks',
'Apparel',
'Financial Services',
'Other',
];
// ============================================================================
// Components
// ============================================================================
function SavedIndicator({ visible }: { visible: boolean }) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: visible ? 1 : 0, x: visible ? 0 : 20 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}
className="flex items-center gap-2 text-performance-green"
>
<CheckCircle className="w-4 h-4" />
<span className="text-sm font-medium">Changes saved</span>
</motion.div>
);
}
// ============================================================================
// Main Component
// ============================================================================
export default function SponsorSettingsPage() {
const shouldReduceMotion = useReducedMotion();
const router = useRouter();
const [profile, setProfile] = useState(MOCK_PROFILE);
const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS);
const [privacy, setPrivacy] = useState(MOCK_PRIVACY);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const handleSaveProfile = async () => {
setSaving(true);
await new Promise(resolve => setTimeout(resolve, 800));
console.log('Profile saved:', profile);
setSaving(false);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
};
const handleDeleteAccount = async () => {
if (confirm('Are you sure you want to delete your sponsor account? This action cannot be undone. All sponsorship data will be permanently removed.')) {
// Call the logout action and handle result
const result = await logoutAction();
if (result.isErr()) {
console.error('Logout failed:', result.getError());
// Could show error toast here
return;
}
// Redirect to login after successful logout
window.location.href = '/auth/login';
setIsDeleting(true);
const result = await logoutAction();
if (result.isErr()) {
console.error('Logout failed:', result.getError());
setIsDeleting(false);
return;
}
router.push('/auth/login');
};
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: shouldReduceMotion ? 0 : 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
const viewData = {
profile: MOCK_PROFILE,
notifications: MOCK_NOTIFICATIONS,
privacy: MOCK_PRIVACY,
};
return (
<Box
maxWidth="4xl"
mx="auto"
py={8}
px={4}
as={motion.div}
// @ts-ignore
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Header */}
<Box as={motion.div} variants={itemVariants}>
<PageHeader
icon={Settings}
title="Sponsor Settings"
description="Manage your company profile, notifications, and security preferences"
action={<SavedIndicator visible={saved} />}
/>
</Box>
{/* Company Profile */}
<Box as={motion.div} variants={itemVariants}>
<Card className="mb-6 overflow-hidden">
<SectionHeader
icon={Building2}
title="Company Profile"
description="Your public-facing company information"
/>
<Box p={6} className="space-y-6">
{/* Company Basic Info */}
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={6}>
<FormField label="Company Name" icon={Building2} required>
<Input
type="text"
value={profile.companyName}
onChange={(e) => setProfile({ ...profile, companyName: e.target.value })}
placeholder="Your company name"
/>
</FormField>
<FormField label="Industry">
<Box as="select"
value={profile.industry}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setProfile({ ...profile, industry: e.target.value })}
w="full"
px={3}
py={2}
bg="bg-iron-gray"
border
borderColor="border-charcoal-outline"
rounded="lg"
color="text-white"
className="focus:outline-none focus:border-primary-blue"
>
{INDUSTRY_OPTIONS.map(industry => (
<option key={industry} value={industry}>{industry}</option>
))}
</Box>
</FormField>
</Box>
{/* Contact Information */}
<Box pt={4} borderTop borderColor="border-charcoal-outline/50">
<Heading level={3} fontSize="sm" weight="semibold" color="text-gray-400" mb={4}>
Contact Information
</Heading>
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={6}>
<FormField label="Contact Name" icon={User} required>
<Input
type="text"
value={profile.contactName}
onChange={(e) => setProfile({ ...profile, contactName: e.target.value })}
placeholder="Full name"
/>
</FormField>
<FormField label="Contact Email" icon={Mail} required>
<Input
type="email"
value={profile.contactEmail}
onChange={(e) => setProfile({ ...profile, contactEmail: e.target.value })}
placeholder="sponsor@company.com"
/>
</FormField>
<FormField label="Phone Number" icon={Phone}>
<Input
type="tel"
value={profile.contactPhone}
onChange={(e) => setProfile({ ...profile, contactPhone: e.target.value })}
placeholder="+1 (555) 123-4567"
/>
</FormField>
<FormField label="Website" icon={Globe}>
<Input
type="url"
value={profile.website}
onChange={(e) => setProfile({ ...profile, website: e.target.value })}
placeholder="https://company.com"
/>
</FormField>
</Box>
</Box>
{/* Address */}
<Box pt={4} borderTop borderColor="border-charcoal-outline/50">
<Heading level={3} fontSize="sm" weight="semibold" color="text-gray-400" mb={4}>
Business Address
</Heading>
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={6}>
<Box colSpan={{ base: 1, md: 2 }}>
<FormField label="Street Address" icon={MapPin}>
<Input
type="text"
value={profile.address.street}
onChange={(e) => setProfile({
...profile,
address: { ...profile.address, street: e.target.value }
})}
placeholder="123 Main Street"
/>
</FormField>
</Box>
<FormField label="City">
<Input
type="text"
value={profile.address.city}
onChange={(e) => setProfile({
...profile,
address: { ...profile.address, city: e.target.value }
})}
placeholder="City"
/>
</FormField>
<FormField label="Postal Code">
<Input
type="text"
value={profile.address.postalCode}
onChange={(e) => setProfile({
...profile,
address: { ...profile.address, postalCode: e.target.value }
})}
placeholder="12345"
/>
</FormField>
<FormField label="Country">
<Input
type="text"
value={profile.address.country}
onChange={(e) => setProfile({
...profile,
address: { ...profile.address, country: e.target.value }
})}
placeholder="Country"
/>
</FormField>
<FormField label="Tax ID / VAT Number" icon={FileText}>
<Input
type="text"
value={profile.taxId}
onChange={(e) => setProfile({ ...profile, taxId: e.target.value })}
placeholder="XX12-3456789"
/>
</FormField>
</Box>
</Box>
{/* Description */}
<Box pt={4} borderTop borderColor="border-charcoal-outline/50">
<FormField label="Company Description">
<Box as="textarea"
value={profile.description}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setProfile({ ...profile, description: e.target.value })}
placeholder="Tell potential sponsorship partners about your company, products, and what you're looking for in sponsorship opportunities..."
rows={4}
w="full"
px={4}
py={3}
bg="bg-iron-gray"
border
borderColor="border-charcoal-outline"
rounded="lg"
color="text-white"
className="placeholder-gray-500 focus:outline-none focus:border-primary-blue resize-none"
/>
<Text size="xs" color="text-gray-500" block mt={1}>
This description appears on your public sponsor profile.
</Text>
</FormField>
</Box>
{/* Social Links */}
<Box pt={4} borderTop borderColor="border-charcoal-outline/50">
<Heading level={3} fontSize="sm" weight="semibold" color="text-gray-400" mb={4}>
Social Media
</Heading>
<Box display="grid" gridCols={{ base: 1, md: 3 }} gap={6}>
<FormField label="Twitter / X" icon={LinkIcon}>
<Input
type="text"
value={profile.socialLinks.twitter}
onChange={(e) => setProfile({
...profile,
socialLinks: { ...profile.socialLinks, twitter: e.target.value }
})}
placeholder="@username"
/>
</FormField>
<FormField label="LinkedIn" icon={LinkIcon}>
<Input
type="text"
value={profile.socialLinks.linkedin}
onChange={(e) => setProfile({
...profile,
socialLinks: { ...profile.socialLinks, linkedin: e.target.value }
})}
placeholder="company-name"
/>
</FormField>
<FormField label="Instagram" icon={LinkIcon}>
<Input
type="text"
value={profile.socialLinks.instagram}
onChange={(e) => setProfile({
...profile,
socialLinks: { ...profile.socialLinks, instagram: e.target.value }
})}
placeholder="@username"
/>
</FormField>
</Box>
</Box>
{/* Logo Upload */}
<Box pt={4} borderTop borderColor="border-charcoal-outline/50">
<FormField label="Company Logo" icon={ImageIcon}>
<Stack direction="row" align="start" gap={6}>
<Box w="24" h="24" rounded="xl" bg="bg-gradient-to-br from-iron-gray to-deep-graphite" border borderColor="border-charcoal-outline" borderStyle="dashed" display="flex" alignItems="center" justifyContent="center" overflow="hidden">
{profile.logoUrl ? (
<Image src={profile.logoUrl} alt="Company logo" width={96} height={96} objectFit="cover" />
) : (
<Building2 className="w-10 h-10 text-gray-600" />
)}
</Box>
<Box flexGrow={1}>
<Stack direction="row" align="center" gap={3}>
<Text as="label" cursor="pointer">
<input
type="file"
accept="image/png,image/jpeg,image/svg+xml"
className="hidden"
/>
<Box px={4} py={2} rounded="lg" bg="bg-iron-gray" border borderColor="border-charcoal-outline" color="text-gray-300" transition className="hover:bg-charcoal-outline" display="flex" alignItems="center" gap={2}>
<Upload className="w-4 h-4" />
<Text>Upload Logo</Text>
</Box>
</Text>
{profile.logoUrl && (
<Button variant="secondary" className="text-sm text-gray-400">
Remove
</Button>
)}
</Stack>
<Text size="xs" color="text-gray-500" block mt={2}>
PNG, JPEG, or SVG. Max 2MB. Recommended size: 400x400px.
</Text>
</Box>
</Stack>
</FormField>
</Box>
{/* Save Button */}
<Box pt={6} borderTop borderColor="border-charcoal-outline" display="flex" alignItems="center" justifyContent="end" gap={4}>
<Button
variant="primary"
onClick={handleSaveProfile}
disabled={saving}
className="min-w-[160px]"
>
{saving ? (
<Stack direction="row" align="center" gap={2}>
<Box w="4" h="4" border borderColor="border-white/30" borderTopColor="border-t-white" rounded="full" animate="spin" />
<Text>Saving...</Text>
</Stack>
) : (
<Stack direction="row" align="center" gap={2}>
<Save className="w-4 h-4" />
<Text>Save Profile</Text>
</Stack>
)}
</Button>
</Box>
</Box>
</Card>
</Box>
{/* Notification Preferences */}
<Box as={motion.div} variants={itemVariants}>
<Card className="mb-6 overflow-hidden">
<SectionHeader
icon={Bell}
title="Email Notifications"
description="Control which emails you receive from GridPilot"
color="text-warning-amber"
/>
<Box p={6}>
<Box className="space-y-1">
<Toggle
checked={notifications.emailNewSponsorships}
onChange={(checked) => setNotifications({ ...notifications, emailNewSponsorships: checked })}
label="Sponsorship Approvals"
description="Receive confirmation when your sponsorship requests are approved"
/>
<Toggle
checked={notifications.emailWeeklyReport}
onChange={(checked) => setNotifications({ ...notifications, emailWeeklyReport: checked })}
label="Weekly Analytics Report"
description="Get a weekly summary of your sponsorship performance"
/>
<Toggle
checked={notifications.emailRaceAlerts}
onChange={(checked) => setNotifications({ ...notifications, emailRaceAlerts: checked })}
label="Race Day Alerts"
description="Be notified when sponsored leagues have upcoming races"
/>
<Toggle
checked={notifications.emailPaymentAlerts}
onChange={(checked) => setNotifications({ ...notifications, emailPaymentAlerts: checked })}
label="Payment & Invoice Notifications"
description="Receive invoices and payment confirmations"
/>
<Toggle
checked={notifications.emailNewOpportunities}
onChange={(checked) => setNotifications({ ...notifications, emailNewOpportunities: checked })}
label="New Sponsorship Opportunities"
description="Get notified about new leagues and drivers seeking sponsors"
/>
<Toggle
checked={notifications.emailContractExpiry}
onChange={(checked) => setNotifications({ ...notifications, emailContractExpiry: checked })}
label="Contract Expiry Reminders"
description="Receive reminders before your sponsorship contracts expire"
/>
</Box>
</Box>
</Card>
</Box>
{/* Privacy & Visibility */}
<Box as={motion.div} variants={itemVariants}>
<Card className="mb-6 overflow-hidden">
<SectionHeader
icon={Eye}
title="Privacy & Visibility"
description="Control how your profile appears to others"
color="text-performance-green"
/>
<Box p={6}>
<Box className="space-y-1">
<Toggle
checked={privacy.publicProfile}
onChange={(checked) => setPrivacy({ ...privacy, publicProfile: checked })}
label="Public Profile"
description="Allow leagues, teams, and drivers to view your sponsor profile"
/>
<Toggle
checked={privacy.showStats}
onChange={(checked) => setPrivacy({ ...privacy, showStats: checked })}
label="Show Sponsorship Statistics"
description="Display your total sponsorships and investment amounts"
/>
<Toggle
checked={privacy.showActiveSponsorships}
onChange={(checked) => setPrivacy({ ...privacy, showActiveSponsorships: checked })}
label="Show Active Sponsorships"
description="Let others see which leagues and teams you currently sponsor"
/>
<Toggle
checked={privacy.allowDirectContact}
onChange={(checked) => setPrivacy({ ...privacy, allowDirectContact: checked })}
label="Allow Direct Contact"
description="Enable leagues and teams to send you sponsorship proposals"
/>
</Box>
</Box>
</Card>
</Box>
{/* Security */}
<Box as={motion.div} variants={itemVariants}>
<Card className="mb-6 overflow-hidden">
<SectionHeader
icon={Shield}
title="Account Security"
description="Protect your sponsor account"
color="text-primary-blue"
/>
<Box p={6} className="space-y-4">
<Box display="flex" alignItems="center" justifyContent="between" py={3} borderBottom borderColor="border-charcoal-outline/50">
<Stack direction="row" align="center" gap={4}>
<Box p={2} rounded="lg" bg="bg-iron-gray">
<Key className="w-5 h-5 text-gray-400" />
</Box>
<Box>
<Text color="text-gray-200" weight="medium" block>Password</Text>
<Text size="sm" color="text-gray-500" block>Last changed 3 months ago</Text>
</Box>
</Stack>
<Button variant="secondary">
Change Password
</Button>
</Box>
<Box display="flex" alignItems="center" justifyContent="between" py={3} borderBottom borderColor="border-charcoal-outline/50">
<Stack direction="row" align="center" gap={4}>
<Box p={2} rounded="lg" bg="bg-iron-gray">
<Smartphone className="w-5 h-5 text-gray-400" />
</Box>
<Box>
<Text color="text-gray-200" weight="medium" block>Two-Factor Authentication</Text>
<Text size="sm" color="text-gray-500" block>Add an extra layer of security to your account</Text>
</Box>
</Stack>
<Button variant="secondary">
Enable 2FA
</Button>
</Box>
<Box display="flex" alignItems="center" justifyContent="between" py={3}>
<Stack direction="row" align="center" gap={4}>
<Box p={2} rounded="lg" bg="bg-iron-gray">
<Lock className="w-5 h-5 text-gray-400" />
</Box>
<Box>
<Text color="text-gray-200" weight="medium" block>Active Sessions</Text>
<Text size="sm" color="text-gray-500" block>Manage devices where you&apos;re logged in</Text>
</Box>
</Stack>
<Button variant="secondary">
View Sessions
</Button>
</Box>
</Box>
</Card>
</Box>
{/* Danger Zone */}
<Box as={motion.div} variants={itemVariants}>
<Card className="border-racing-red/30 overflow-hidden">
<Box p={5} borderBottom borderColor="border-racing-red/30" bg="bg-gradient-to-r from-racing-red/10 to-transparent">
<Heading level={2} fontSize="lg" weight="semibold" color="text-racing-red" icon={<Box p={2} rounded="lg" bg="bg-racing-red/10"><AlertCircle className="w-5 h-5 text-racing-red" /></Box>}>
Danger Zone
</Heading>
</Box>
<Box p={6}>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" gap={4}>
<Box p={2} rounded="lg" bg="bg-racing-red/10">
<Trash2 className="w-5 h-5 text-racing-red" />
</Box>
<Box>
<Text color="text-gray-200" weight="medium" block>Delete Sponsor Account</Text>
<Text size="sm" color="text-gray-500" block>
Permanently delete your account and all associated sponsorship data.
This action cannot be undone.
</Text>
</Box>
</Stack>
<Button
variant="secondary"
onClick={handleDeleteAccount}
className="text-racing-red border-racing-red/30 hover:bg-racing-red/10"
>
Delete Account
</Button>
</Box>
</Box>
</Card>
</Box>
</Box>
<>
<SponsorSettingsTemplate
viewData={viewData}
profile={profile}
setProfile={setProfile as any}
notifications={notifications}
setNotifications={setNotifications as any}
onSaveProfile={handleSaveProfile}
onDeleteAccount={() => setIsDeleteDialogOpen(true)}
saving={saving}
saved={saved}
/>
<ConfirmDialog
isOpen={isDeleteDialogOpen}
onClose={() => setIsDeleteDialogOpen(false)}
onConfirm={handleDeleteAccount}
title="Delete Sponsor Account"
description="Are you sure you want to delete your sponsor account? This action cannot be undone. All sponsorship data, contracts, and history will be permanently removed."
confirmLabel="Delete Account"
variant="danger"
isLoading={isDeleting}
/>
</>
);
}