website refactor

This commit is contained in:
2026-01-15 17:12:24 +01:00
parent c3b308e960
commit f035cfe7ce
468 changed files with 24378 additions and 17324 deletions

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { SponsorActivityItem } from '@/ui/SponsorActivityItem';
interface ActivityItemProps {
activity: {
@@ -15,20 +13,11 @@ interface ActivityItemProps {
export function ActivityItem({ activity }: ActivityItemProps) {
return (
<Stack direction="row" align="start" gap={3} style={{ padding: '0.75rem 0', borderBottom: '1px solid rgba(38, 38, 38, 0.5)' }}>
<Box style={{ width: '0.5rem', height: '0.5rem', borderRadius: '9999px', marginTop: '0.5rem', backgroundColor: activity.typeColor }} />
<Box style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" color="text-white" style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{activity.message}</Text>
<Stack direction="row" align="center" gap={2} style={{ marginTop: '0.25rem' }}>
<Text size="xs" color="text-gray-500">{activity.time}</Text>
{activity.formattedImpressions && (
<>
<Text size="xs" color="text-gray-600"></Text>
<Text size="xs" color="text-gray-400">{activity.formattedImpressions} views</Text>
</>
)}
</Stack>
</Box>
</Stack>
<SponsorActivityItem
message={activity.message}
time={activity.time}
typeColor={activity.typeColor}
formattedImpressions={activity.formattedImpressions}
/>
);
}

View File

@@ -1,17 +1,7 @@
'use client';
import React from 'react';
import { Star, Clock, CheckCircle2, ChevronRight } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { Button } from '@/ui/Button';
import { Link } from '@/ui/Link';
import { AvailableLeagueCard as UiAvailableLeagueCard } from '@/ui/AvailableLeagueCard';
interface AvailableLeague {
id: string;
@@ -35,129 +25,5 @@ interface AvailableLeagueCardProps {
}
export function AvailableLeagueCard({ league }: AvailableLeagueCardProps) {
const tierConfig = {
premium: { icon: '⭐', label: 'Premium' },
standard: { icon: '🏆', label: 'Standard' },
starter: { icon: '🚀', label: 'Starter' },
};
const statusConfig = {
active: { color: '#10b981', label: 'Active Season' },
upcoming: { color: '#f59e0b', label: 'Starting Soon' },
completed: { color: '#9ca3af', label: 'Season Ended' },
};
const config = tierConfig[league.tier];
const status = statusConfig[league.seasonStatus];
return (
<Card>
<Stack gap={4}>
{/* Header */}
<Stack direction="row" align="start" justify="between">
<Box style={{ flex: 1 }}>
<Stack direction="row" align="center" gap={2} mb={1} wrap>
<Badge variant="primary">{config.icon} {config.label}</Badge>
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: `${status.color}1A`, paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<Text size="xs" weight="medium" style={{ color: status.color }}>{status.label}</Text>
</Surface>
</Stack>
<Heading level={3}>{league.name}</Heading>
<Text size="sm" color="text-gray-500" block mt={1}>{league.game}</Text>
</Box>
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Star} size={3.5} color="#facc15" />
<Text size="sm" weight="medium" color="text-white">{league.rating}</Text>
</Stack>
</Surface>
</Stack>
{/* Description */}
<Text size="sm" color="text-gray-400" block truncate>{league.description}</Text>
{/* Stats Grid */}
<Box style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: '0.5rem' }}>
<StatItem label="Drivers" value={league.drivers} />
<StatItem label="Avg Views" value={league.formattedAvgViews} />
<StatItem label="CPM" value={league.formattedCpm} color="#10b981" />
</Box>
{/* Next Race */}
{league.nextRace && (
<Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} color="#9ca3af" />
<Text size="sm" color="text-gray-400">Next:</Text>
<Text size="sm" color="text-white">{league.nextRace}</Text>
</Stack>
)}
{/* Sponsorship Slots */}
<Stack gap={2}>
<SlotRow
label="Main Sponsor"
available={league.mainSponsorSlot.available}
price={`$${league.mainSponsorSlot.price}/season`}
/>
<SlotRow
label="Secondary Slots"
available={league.secondarySlots.available > 0}
price={`${league.secondarySlots.available}/${league.secondarySlots.total} @ $${league.secondarySlots.price}`}
/>
</Stack>
{/* Actions */}
<Stack direction="row" gap={2}>
<Box style={{ flex: 1 }}>
<Link href={`/sponsor/leagues/${league.id}`}>
<Button variant="secondary" fullWidth size="sm">
View Details
</Button>
</Link>
</Box>
{(league.mainSponsorSlot.available || league.secondarySlots.available > 0) && (
<Box style={{ flex: 1 }}>
<Link href={`/sponsor/leagues/${league.id}?action=sponsor`}>
<Button variant="primary" fullWidth size="sm">
Sponsor
</Button>
</Link>
</Box>
)}
</Stack>
</Stack>
</Card>
);
}
function StatItem({ label, value, color = 'white' }: { label: string, value: string | number, color?: string }) {
return (
<Box p={2} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', borderRadius: '0.5rem', textAlign: 'center' }}>
<Text weight="bold" color="text-white" style={{ color: color !== 'white' ? color : undefined }}>{value}</Text>
<Text size="xs" color="text-gray-500" block mt={1}>{label}</Text>
</Box>
);
}
function SlotRow({ label, available, price }: { label: string, available: boolean, price: string }) {
return (
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(38, 38, 38, 0.3)' }}>
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={2}>
<Box style={{ width: '0.625rem', height: '0.625rem', borderRadius: '9999px', backgroundColor: available ? '#10b981' : '#ef4444' }} />
<Text size="sm" color="text-gray-300">{label}</Text>
</Stack>
<Box>
{available ? (
<Text size="sm" weight="semibold" color="text-white">{price}</Text>
) : (
<Stack direction="row" align="center" gap={1}>
<Icon icon={CheckCircle2} size={3} color="#737373" />
<Text size="sm" color="text-gray-500">Filled</Text>
</Stack>
)}
</Box>
</Stack>
</Surface>
);
return <UiAvailableLeagueCard league={league} />;
}

View File

@@ -1,10 +1,8 @@
'use client';
import React from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import { ArrowUpRight, ArrowDownRight, LucideIcon } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { LucideIcon } from 'lucide-react';
import { StatCard } from '@/ui/StatCard';
interface MetricCardProps {
title: string;
@@ -20,43 +18,23 @@ export function MetricCard({
title,
value,
change,
icon: Icon,
icon,
suffix = '',
prefix = '',
delay = 0,
}: MetricCardProps) {
const shouldReduceMotion = useReducedMotion();
const isPositive = change && change > 0;
const isNegative = change && change < 0;
return (
<motion.div
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
style={{ height: '100%' }}
>
<Card style={{ height: '100%', padding: '1.25rem' }}>
<Stack gap={3}>
<Stack direction="row" align="start" justify="between">
<Box style={{ display: 'flex', height: '2.75rem', width: '2.75rem', alignItems: 'center', justifyContent: 'center', borderRadius: '0.75rem', backgroundColor: 'rgba(59, 130, 246, 0.1)' }}>
<Icon style={{ width: '1.25rem', height: '1.25rem', color: '#3b82f6' }} />
</Box>
{change !== undefined && (
<Stack direction="row" align="center" gap={1} style={{ fontSize: '0.875rem', fontWeight: 500, color: isPositive ? '#10b981' : isNegative ? '#ef4444' : '#9ca3af' }}>
{isPositive ? <ArrowUpRight style={{ width: '1rem', height: '1rem' }} /> : isNegative ? <ArrowDownRight style={{ width: '1rem', height: '1rem' }} /> : null}
<Text size="sm" weight="medium">{Math.abs(change)}%</Text>
</Stack>
)}
</Stack>
<Box>
<Text size="2xl" weight="bold" color="text-white" style={{ marginBottom: '0.25rem', display: 'block' }}>
{prefix}{typeof value === 'number' ? value.toLocaleString() : value}{suffix}
</Text>
<Text size="sm" color="text-gray-400">{title}</Text>
</Box>
</Stack>
</Card>
</motion.div>
<StatCard
label={title}
value={value}
icon={icon}
trend={change !== undefined ? {
value: Math.abs(change),
isPositive: change > 0
} : undefined}
suffix={suffix}
prefix={prefix}
delay={delay}
/>
);
}

View File

@@ -1,21 +1,26 @@
'use client';
import React, { useState, useEffect } from 'react';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import { Clock, Check, X, DollarSign, MessageCircle, User, Building } from 'lucide-react';
import React, { useState } from 'react';
import { Building } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
import { SponsorshipRequestItem } from '@/ui/SponsorshipRequestItem';
export interface PendingRequestDTO {
id: string;
sponsorId: string;
sponsorName: string;
sponsorLogo?: string | undefined;
tier: 'main' | 'secondary';
tier: string;
offeredAmount: number;
currency: string;
formattedAmount: string;
message?: string | undefined;
createdAt: Date;
createdAt: string | Date;
platformFee: number;
netAmount: number;
}
@@ -30,10 +35,8 @@ interface PendingSponsorshipRequestsProps {
isLoading?: boolean;
}
export default function PendingSponsorshipRequests({
export function PendingSponsorshipRequests({
entityType,
entityId,
entityName,
requests,
onAccept,
onReject,
@@ -65,177 +68,67 @@ export default function PendingSponsorshipRequests({
if (isLoading) {
return (
<div className="text-center py-8 text-gray-400">
<div className="animate-pulse">Loading sponsorship requests...</div>
</div>
<Box textAlign="center" py={8}>
<Text color="text-gray-400" animate="pulse">Loading sponsorship requests...</Text>
</Box>
);
}
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">
<Box textAlign="center" py={8}>
<Box w="12" h="12" mx="auto" mb={3} rounded="full" bg="bg-iron-gray/50" display="flex" alignItems="center" justifyContent="center">
<Icon icon={Building} size={6} color="rgb(115, 115, 115)" />
</Box>
<Text color="text-gray-400" size="sm" block>No pending sponsorship requests</Text>
<Text color="text-gray-500" size="xs" mt={1} block>
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>
</Text>
</Box>
);
}
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">
<Stack gap={4}>
<Box display="flex" alignItems="center" justifyContent="between">
<Heading level={3}>Sponsorship Requests</Heading>
<Badge variant="primary">
{requests.length} pending
</span>
</div>
</Badge>
</Box>
<div className="space-y-3">
{requests.map((request) => {
const isProcessing = processingId === request.id;
const isRejecting = rejectModalId === request.id;
<Stack gap={3}>
{requests.map((request) => (
<SponsorshipRequestItem
key={request.id}
sponsorName={request.sponsorName}
sponsorLogo={request.sponsorLogo}
tier={request.tier}
formattedAmount={request.formattedAmount}
netAmount={request.netAmount}
createdAt={typeof request.createdAt === 'string' ? new Date(request.createdAt) : request.createdAt}
message={request.message}
isProcessing={processingId === request.id}
isRejecting={rejectModalId === request.id}
rejectReason={rejectReason}
onAccept={() => handleAccept(request.id)}
onRejectClick={() => setRejectModalId(request.id)}
onRejectConfirm={() => handleReject(request.id)}
onRejectCancel={() => {
setRejectModalId(null);
setRejectReason('');
}}
onRejectReasonChange={setRejectReason}
/>
))}
</Stack>
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>
<Box mt={4}>
<Text size="xs" color="text-gray-500" block>
<Text weight="bold" color="text-gray-400">Note:</Text> Accepting a request will activate the sponsorship.
The sponsor will be charged per season and you&apos;ll receive the payment minus 10% platform fee.
</Text>
</Box>
</Stack>
);
}

View File

@@ -1,9 +1,8 @@
'use client';
import React from 'react';
import { Trophy, Users, Car, Flag, Megaphone, LucideIcon } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { RenewalItem } from '@/ui/RenewalItem';
interface RenewalAlertProps {
renewal: {
@@ -26,20 +25,11 @@ export function RenewalAlert({ renewal }: RenewalAlertProps) {
const Icon = typeIcons[renewal.type] || Trophy;
return (
<Stack direction="row" align="center" justify="between" style={{ padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: 'rgba(245, 158, 11, 0.1)', border: '1px solid rgba(245, 158, 11, 0.3)' }}>
<Stack direction="row" align="center" gap={3}>
<Icon style={{ width: '1rem', height: '1rem', color: '#f59e0b' }} />
<Box>
<Text size="sm" color="text-white" style={{ display: 'block' }}>{renewal.name}</Text>
<Text size="xs" color="text-gray-400">Renews {renewal.formattedRenewDate}</Text>
</Box>
</Stack>
<Box style={{ textAlign: 'right' }}>
<Text size="sm" weight="semibold" color="text-white" style={{ display: 'block' }}>{renewal.formattedPrice}</Text>
<Button variant="secondary" style={{ fontSize: '0.75rem', marginTop: '0.25rem', padding: '0.25rem 0.5rem', minHeight: 0 }}>
Renew
</Button>
</Box>
</Stack>
<RenewalItem
name={renewal.name}
renewDateLabel={renewal.formattedRenewDate}
priceLabel={renewal.formattedPrice}
icon={Icon}
/>
);
}

View File

@@ -1,8 +1,8 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { ReactNode, useEffect, useState } from 'react';
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { BenefitCard } from '@/ui/BenefitCard';
interface SponsorBenefitCardProps {
icon: LucideIcon;
@@ -16,82 +16,22 @@ interface SponsorBenefitCardProps {
delay?: number;
}
export default function SponsorBenefitCard({
icon: Icon,
export function SponsorBenefitCard({
icon,
title,
description,
stats,
variant = 'default',
delay = 0,
}: SponsorBenefitCardProps) {
const shouldReduceMotion = useReducedMotion();
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const isHighlight = variant === 'highlight';
const cardContent = (
<div
className={`
relative h-full rounded-xl p-6 transition-all duration-300
${isHighlight
? 'bg-gradient-to-br from-primary-blue/10 to-primary-blue/5 border border-primary-blue/30'
: 'bg-iron-gray/50 border border-charcoal-outline hover:border-charcoal-outline/80'
}
`}
>
{/* Icon */}
<div
className={`
w-12 h-12 rounded-xl flex items-center justify-center mb-4
${isHighlight
? 'bg-primary-blue/20'
: 'bg-iron-gray border border-charcoal-outline'
}
`}
>
<Icon className={`w-6 h-6 ${isHighlight ? 'text-primary-blue' : 'text-gray-400'}`} />
</div>
{/* Content */}
<h3 className="text-lg font-semibold text-white mb-2">{title}</h3>
<p className="text-sm text-gray-400 leading-relaxed">{description}</p>
{/* Stats */}
{stats && (
<div className="mt-4 pt-4 border-t border-charcoal-outline/50">
<div className="flex items-baseline gap-2">
<span className={`text-2xl font-bold ${isHighlight ? 'text-primary-blue' : 'text-white'}`}>
{stats.value}
</span>
<span className="text-sm text-gray-500">{stats.label}</span>
</div>
</div>
)}
{/* Highlight Glow Effect */}
{isHighlight && (
<div className="absolute -inset-px rounded-xl bg-gradient-to-br from-primary-blue/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
)}
</div>
);
if (!isMounted || shouldReduceMotion) {
return <div className="group">{cardContent}</div>;
}
return (
<motion.div
className="group"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay }}
whileHover={{ y: -4, transition: { duration: 0.2 } }}
>
{cardContent}
</motion.div>
<BenefitCard
icon={icon}
title={title}
description={description}
stats={stats}
variant={variant}
delay={delay}
/>
);
}
}

View File

@@ -1,144 +0,0 @@
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { ReactNode, useEffect, useState } from 'react';
import { Building2, TrendingUp, Eye, Users, ChevronRight } from 'lucide-react';
interface SponsorHeroProps {
title: string;
subtitle: string;
children?: ReactNode;
}
export default function SponsorHero({ title, subtitle, children }: SponsorHeroProps) {
const shouldReduceMotion = useReducedMotion();
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: 'easeOut' as const },
},
};
if (!isMounted || shouldReduceMotion) {
return (
<div className="relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 bg-gradient-to-br from-primary-blue/5 via-transparent to-transparent" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-primary-blue/10 via-transparent to-transparent" />
{/* Grid Pattern */}
<div
className="absolute inset-0 opacity-5"
style={{
backgroundImage: `
linear-gradient(to right, #198CFF 1px, transparent 1px),
linear-gradient(to bottom, #198CFF 1px, transparent 1px)
`,
backgroundSize: '40px 40px',
}}
/>
<div className="relative max-w-5xl mx-auto px-4 py-16 sm:py-24">
<div className="text-center">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary-blue/10 border border-primary-blue/20 mb-6">
<Building2 className="w-4 h-4 text-primary-blue" />
<span className="text-sm text-primary-blue font-medium">Sponsor Portal</span>
</div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6 tracking-tight">
{title}
</h1>
<p className="text-lg sm:text-xl text-gray-400 max-w-2xl mx-auto mb-10">
{subtitle}
</p>
{children}
</div>
</div>
</div>
);
}
return (
<motion.div
className="relative overflow-hidden"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* Background Pattern */}
<div className="absolute inset-0 bg-gradient-to-br from-primary-blue/5 via-transparent to-transparent" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-primary-blue/10 via-transparent to-transparent" />
{/* Animated Grid Pattern */}
<motion.div
className="absolute inset-0 opacity-5"
style={{
backgroundImage: `
linear-gradient(to right, #198CFF 1px, transparent 1px),
linear-gradient(to bottom, #198CFF 1px, transparent 1px)
`,
backgroundSize: '40px 40px',
}}
animate={{
backgroundPosition: ['0px 0px', '40px 40px'],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: 'linear' as const,
}}
/>
<div className="relative max-w-5xl mx-auto px-4 py-16 sm:py-24">
<div className="text-center">
<motion.div
variants={itemVariants}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary-blue/10 border border-primary-blue/20 mb-6"
>
<Building2 className="w-4 h-4 text-primary-blue" />
<span className="text-sm text-primary-blue font-medium">Sponsor Portal</span>
</motion.div>
<motion.h1
variants={itemVariants}
className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6 tracking-tight"
>
{title}
</motion.h1>
<motion.p
variants={itemVariants}
className="text-lg sm:text-xl text-gray-400 max-w-2xl mx-auto mb-10"
>
{subtitle}
</motion.p>
<motion.div variants={itemVariants}>
{children}
</motion.div>
</div>
</div>
</motion.div>
);
}

View File

@@ -1,70 +1,53 @@
'use client';
import Button from '@/ui/Button';
import Card from '@/ui/Card';
import { useInject } from '@/lib/di/hooks/useInject';
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
import React, { useCallback, useState } from 'react';
import {
Activity,
Check,
Loader2,
MessageCircle,
Shield,
Target
Target,
LucideIcon
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import React, { useCallback, useState } from 'react';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { SponsorMetricCard } from '@/ui/SponsorMetricCard';
import { SponsorSlotCard } from '@/ui/SponsorSlotCard';
import { SponsorshipTierBadge } from '@/ui/SponsorshipTierBadge';
import { InfoBox } from '@/ui/InfoBox';
import { SponsorMetric, SponsorshipSlot } from './SponsorInsightsCardTypes';
import { getTierStyles, getEntityLabel, getEntityIcon, getSponsorshipTagline } from './SponsorInsightsCardHelpers';
// ============================================================================
// TYPES
// ============================================================================
import { getTierStyles, getEntityLabel, getSponsorshipTagline } from './SponsorInsightsCardHelpers';
export type EntityType = 'league' | 'race' | 'driver' | 'team';
export interface SponsorInsightsProps {
// Entity info
entityType: EntityType;
entityId: string;
entityName: string;
// Tier classification
tier: 'premium' | 'standard' | 'starter';
// Key metrics (shown in grid)
metrics: SponsorMetric[];
// Sponsorship availability
slots: SponsorshipSlot[];
// Optional: additional stats section
additionalStats?: {
label: string;
items: Array<{ label: string; value: string | number }>;
};
// Optional: trust indicators
trustScore?: number;
discordMembers?: number;
monthlyActivity?: number;
// CTA customization
ctaLabel?: string;
ctaHref?: string;
// Optional: current sponsor ID (if logged in as sponsor)
currentSponsorId?: string;
// Optional: callback when sponsorship request is submitted
onSponsorshipRequested?: (tier: 'main' | 'secondary') => void;
onNavigate: (href: string) => void;
}
// ============================================================================
// COMPONENT
// ============================================================================
export default function SponsorInsightsCard({
export function SponsorInsightsCard({
entityType,
entityId,
entityName,
@@ -79,14 +62,10 @@ export default function SponsorInsightsCard({
ctaHref,
currentSponsorId,
onSponsorshipRequested,
onNavigate,
}: SponsorInsightsProps) {
// TODO components should not fetch any data
const router = useRouter();
const sponsorshipService = useInject(SPONSOR_SERVICE_TOKEN);
const tierStyles = getTierStyles(tier);
const EntityIcon = getEntityIcon(entityType);
// State for sponsorship application
const [applyingTier, setApplyingTier] = useState<'main' | 'secondary' | null>(null);
const [appliedTiers, setAppliedTiers] = useState<Set<'main' | 'secondary'>>(new Set());
const [error, setError] = useState<string | null>(null);
@@ -95,10 +74,9 @@ export default function SponsorInsightsCard({
const secondarySlots = slots.filter(s => s.tier === 'secondary');
const availableSecondary = secondarySlots.filter(s => s.available).length;
// Map EntityType to SponsorableEntityType
const getSponsorableEntityType = useCallback((type: EntityType): 'driver' | 'team' | 'race' | 'season' => {
switch (type) {
case 'league': return 'season'; // Leagues are sponsored via their seasons
case 'league': return 'season';
case 'race': return 'race';
case 'driver': return 'driver';
case 'team': return 'team';
@@ -106,20 +84,17 @@ export default function SponsorInsightsCard({
}, []);
const handleSponsorClick = useCallback(async (slotTier: 'main' | 'secondary') => {
// If no sponsor ID, redirect to sponsor signup/login
if (!currentSponsorId) {
const href = ctaHref || `/sponsor/${entityType}s/${entityId}?tier=${slotTier}`;
router.push(href);
onNavigate(href);
return;
}
// If already applied for this tier, show details page
if (appliedTiers.has(slotTier)) {
router.push(`/sponsor/dashboard`);
onNavigate(`/sponsor/dashboard`);
return;
}
// Apply for sponsorship using service
setApplyingTier(slotTier);
setError(null);
@@ -127,26 +102,18 @@ export default function SponsorInsightsCard({
const slot = slotTier === 'main' ? mainSlot : secondarySlots[0];
const slotPrice = slot?.price ?? 0;
// Note: The sponsorship service would need a method to submit sponsorship requests
// For now, we'll use a placeholder since the exact API may not be available
const request = {
sponsorId: currentSponsorId,
entityType: getSponsorableEntityType(entityType),
entityId,
tier: slotTier,
offeredAmount: slotPrice * 100, // Convert to cents
offeredAmount: slotPrice * 100,
currency: (slot?.currency as 'USD' | 'EUR' | 'GBP') ?? 'USD',
message: `Interested in sponsoring ${entityName} as ${slotTier} sponsor.`,
};
// This would be: await sponsorshipService.submitSponsorshipRequest(request);
// For now, we'll log it as a placeholder
console.log('Sponsorship request:', request);
// Mark as applied
setAppliedTiers(prev => new Set([...prev, slotTier]));
// Call callback if provided
onSponsorshipRequested?.(slotTier);
} catch (err) {
@@ -155,214 +122,174 @@ export default function SponsorInsightsCard({
} finally {
setApplyingTier(null);
}
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, router, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, onNavigate, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
return (
<Card className={`mb-6 border-primary-blue/30 bg-gradient-to-r ${tierStyles.gradient}`}>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Target className="w-5 h-5 text-primary-blue" />
<h3 className="text-lg font-semibold text-white">Sponsorship Opportunity</h3>
</div>
<p className="text-gray-400 text-sm">
<Card
mb={6}
borderColor="border-primary-blue/30"
bg={`linear-gradient(to right, ${tierStyles.gradient.split(' ')[1]}, ${tierStyles.gradient.split(' ')[2]})`}
>
<Box display="flex" alignItems="start" justifyContent="between" mb={4}>
<Box>
<Stack direction="row" align="center" gap={2} mb={1}>
<Icon icon={Target} size={5} color="rgb(59, 130, 246)" />
<Heading level={3}>Sponsorship Opportunity</Heading>
</Stack>
<Text size="sm" color="text-gray-400">
{getSponsorshipTagline(entityType)}
</p>
</div>
<div className="flex items-center gap-2">
<div className={`px-2 py-1 rounded text-xs font-medium border ${tierStyles.badge}`}>
{tier.charAt(0).toUpperCase() + tier.slice(1)} {getEntityLabel(entityType)}
</div>
</div>
</div>
</Text>
</Box>
<SponsorshipTierBadge tier={tier} entityLabel={getEntityLabel(entityType)} />
</Box>
{/* Key Metrics Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
{metrics.slice(0, 4).map((metric, index) => {
const Icon = metric.icon as React.ComponentType<{ className?: string }>;
return (
<div
key={index}
className="bg-iron-gray/50 rounded-lg p-3 border border-charcoal-outline"
>
<div className={`flex items-center gap-1.5 ${metric.color || 'text-primary-blue'} mb-1`}>
<Icon className="w-4 h-4" />
<span className="text-xs font-medium">{metric.label}</span>
</div>
<div className="flex items-baseline gap-2">
<div className="text-xl font-bold text-white">
{typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value}
</div>
{metric.trend && (
<span className={`text-xs ${metric.trend.isPositive ? 'text-performance-green' : 'text-red-400'}`}>
{metric.trend.isPositive ? '+' : ''}{metric.trend.value}%
</span>
)}
</div>
</div>
);
})}
</div>
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={3} mb={4}>
{metrics.slice(0, 4).map((metric, index) => (
<SponsorMetricCard
key={index}
label={metric.label}
value={metric.value}
icon={metric.icon as LucideIcon}
color={metric.color}
trend={metric.trend}
/>
))}
</Box>
{/* Trust & Activity Indicators */}
{(trustScore !== undefined || discordMembers !== undefined || monthlyActivity !== undefined) && (
<div className="flex flex-wrap gap-4 mb-4 pb-4 border-b border-charcoal-outline/50">
<Box display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
{trustScore !== undefined && (
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-performance-green" />
<span className="text-sm text-gray-400">Trust Score:</span>
<span className="text-sm font-semibold text-white">{trustScore}/100</span>
</div>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Shield} size={4} color="rgb(16, 185, 129)" />
<Text size="sm" color="text-gray-400">Trust Score:</Text>
<Text size="sm" weight="semibold" color="text-white">{trustScore}/100</Text>
</Stack>
)}
{discordMembers !== undefined && (
<div className="flex items-center gap-2">
<MessageCircle className="w-4 h-4 text-purple-400" />
<span className="text-sm text-gray-400">Discord:</span>
<span className="text-sm font-semibold text-white">{discordMembers.toLocaleString()}</span>
</div>
<Stack direction="row" align="center" gap={2}>
<Icon icon={MessageCircle} size={4} color="rgb(168, 85, 247)" />
<Text size="sm" color="text-gray-400">Discord:</Text>
<Text size="sm" weight="semibold" color="text-white">{discordMembers.toLocaleString()}</Text>
</Stack>
)}
{monthlyActivity !== undefined && (
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-neon-aqua" />
<span className="text-sm text-gray-400">Monthly Activity:</span>
<span className="text-sm font-semibold text-white">{monthlyActivity}%</span>
</div>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Activity} size={4} color="rgb(0, 255, 255)" />
<Text size="sm" color="text-gray-400">Monthly Activity:</Text>
<Text size="sm" weight="semibold" color="text-white">{monthlyActivity}%</Text>
</Stack>
)}
</div>
</Box>
)}
{/* Sponsorship Slots */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
{/* Main Sponsor Slot */}
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={3} mb={4}>
{mainSlot && (
<div className={`p-3 rounded-lg border ${
mainSlot.available
? 'bg-performance-green/10 border-performance-green/30'
: 'bg-iron-gray/30 border-charcoal-outline'
}`}>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-white">Main Sponsor Slot</span>
<span className={`text-xs ${mainSlot.available ? 'text-performance-green' : 'text-gray-500'}`}>
{mainSlot.available ? 'Available' : 'Taken'}
</span>
</div>
<p className="text-xs text-gray-400 mb-2">{mainSlot.benefits.join('')}</p>
{mainSlot.available && (
<div className="flex items-center justify-between">
<span className="text-lg font-bold text-white">
${mainSlot.price.toLocaleString()}/season
</span>
<Button
variant="primary"
onClick={() => handleSponsorClick('main')}
disabled={applyingTier === 'main'}
className="text-xs px-3 py-1"
>
{applyingTier === 'main' ? (
<>
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Applying...
</>
) : appliedTiers.has('main') ? (
<>
<Check className="w-3 h-3 mr-1" />
Applied
</>
) : (
'Apply to Sponsor'
)}
</Button>
</div>
)}
</div>
<SponsorSlotCard
variant="main"
title="Main Sponsor Slot"
status={mainSlot.available ? 'Available' : 'Taken'}
statusColor={mainSlot.available ? 'text-performance-green' : 'text-gray-500'}
benefits={mainSlot.benefits.join(' • ')}
available={mainSlot.available}
price={`$${mainSlot.price.toLocaleString()}/season`}
action={
<Button
variant="primary"
onClick={() => handleSponsorClick('main')}
disabled={applyingTier === 'main'}
size="sm"
>
{applyingTier === 'main' ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Loader2} size={3} animate="spin" />
Applying...
</Stack>
) : appliedTiers.has('main') ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Check} size={3} />
Applied
</Stack>
) : (
'Apply to Sponsor'
)}
</Button>
}
/>
)}
{/* Secondary Slots */}
{secondarySlots.length > 0 && (
<div className={`p-3 rounded-lg border ${
availableSecondary > 0
? 'bg-purple-500/10 border-purple-500/30'
: 'bg-iron-gray/30 border-charcoal-outline'
}`}>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-white">Secondary Slots</span>
<span className={`text-xs ${availableSecondary > 0 ? 'text-purple-400' : 'text-gray-500'}`}>
{availableSecondary}/{secondarySlots.length} Available
</span>
</div>
<p className="text-xs text-gray-400 mb-2">
{secondarySlots[0]?.benefits.join(' • ') || 'Logo placement on page'}
</p>
{availableSecondary > 0 && (
<div className="flex items-center justify-between">
<span className="text-lg font-bold text-white">
${secondarySlots[0]?.price.toLocaleString()}/season
</span>
<Button
variant="secondary"
onClick={() => handleSponsorClick('secondary')}
disabled={applyingTier === 'secondary'}
className="text-xs px-3 py-1"
>
{applyingTier === 'secondary' ? (
<>
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Applying...
</>
) : appliedTiers.has('secondary') ? (
<>
<Check className="w-3 h-3 mr-1" />
Applied
</>
) : (
'Apply to Sponsor'
)}
</Button>
</div>
)}
</div>
<SponsorSlotCard
variant="secondary"
title="Secondary Slots"
status={`${availableSecondary}/${secondarySlots.length} Available`}
statusColor={availableSecondary > 0 ? 'text-purple-400' : 'text-gray-500'}
benefits={secondarySlots[0]?.benefits.join(' • ') || 'Logo placement on page'}
available={availableSecondary > 0}
price={`$${secondarySlots[0]?.price.toLocaleString()}/season`}
action={
<Button
variant="secondary"
onClick={() => handleSponsorClick('secondary')}
disabled={applyingTier === 'secondary'}
size="sm"
>
{applyingTier === 'secondary' ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Loader2} size={3} animate="spin" />
Applying...
</Stack>
) : appliedTiers.has('secondary') ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Check} size={3} />
Applied
</Stack>
) : (
'Apply to Sponsor'
)}
</Button>
}
/>
)}
</div>
</Box>
{/* Additional Stats */}
{additionalStats && (
<div className="mb-4 pb-4 border-b border-charcoal-outline/50">
<h4 className="text-sm font-medium text-gray-400 mb-2">{additionalStats.label}</h4>
<div className="flex flex-wrap gap-4">
<Box mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
<Heading level={4} mb={2} color="text-gray-400">{additionalStats.label}</Heading>
<Box display="flex" flexWrap="wrap" gap={4}>
{additionalStats.items.map((item, index) => (
<div key={index} className="flex items-center gap-2">
<span className="text-sm text-gray-500">{item.label}:</span>
<span className="text-sm font-semibold text-white">
<Stack key={index} direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-500">{item.label}:</Text>
<Text size="sm" weight="semibold" color="text-white">
{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
</span>
</div>
</Text>
</Stack>
))}
</div>
</div>
</Box>
</Box>
)}
{/* Error Message */}
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<p className="text-sm text-red-400">{error}</p>
</div>
<InfoBox
variant="warning"
icon={Target}
title="Error"
description={error}
/>
)}
{/* Footer */}
<div className="flex items-center justify-between pt-3 border-t border-charcoal-outline/50">
<p className="text-xs text-gray-500">
10% platform fee applies Logos burned on all liveries Sponsorships are attached to seasons, so you can change partners from season to season
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-charcoal-outline/50">
<Text size="xs" color="text-gray-500">
10% platform fee applies Logos burned on all liveries Sponsorships are attached to seasons
{appliedTiers.size > 0 && ' • Application pending review'}
</p>
</Text>
<Button
variant="secondary"
onClick={() => router.push(ctaHref || `/sponsor/${entityType}s/${entityId}`)}
className="text-xs"
onClick={() => onNavigate(ctaHref || `/sponsor/${entityType}s/${entityId}`)}
size="sm"
>
{ctaLabel || 'View Full Details'}
</Button>
</div>
</Box>
</Card>
);
}
}

View File

@@ -1,5 +1,5 @@
import { EntityType } from './SponsorInsightsCard';
import { Trophy, Zap, Users, Eye, TrendingUp, Star, Calendar, MessageCircle, Activity, Shield, Target } from 'lucide-react';
import { Trophy, Zap, Users } from 'lucide-react';
export interface TierStyles {
badge: string;

View File

@@ -1,30 +0,0 @@
/**
* SponsorLogo
*
* Pure UI component for displaying sponsor logos.
* Renders an optimized image with fallback on error.
*/
import Image from 'next/image';
export interface SponsorLogoProps {
sponsorId: string;
alt: string;
className?: string;
}
export function SponsorLogo({ sponsorId, alt, className = '' }: SponsorLogoProps) {
return (
<Image
src={`/media/sponsors/${sponsorId}/logo`}
alt={alt}
width={100}
height={100}
className={`object-contain ${className}`}
onError={(e) => {
// Fallback to default logo
(e.target as HTMLImageElement).src = '/default-sponsor-logo.png';
}}
/>
);
}

View File

@@ -1,15 +1,8 @@
'use client';
import React from 'react';
import { Trophy, Star, CheckCircle2 } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { Trophy, Star } from 'lucide-react';
import { SponsorTierCard as UiSponsorTierCard } from '@/ui/SponsorTierCard';
interface SponsorTierCardProps {
type: 'main' | 'secondary';
@@ -34,63 +27,20 @@ export function SponsorTierCard({
}: SponsorTierCardProps) {
const isMain = type === 'main';
const TierIcon = isMain ? Trophy : Star;
const iconColor = isMain ? '#facc15' : '#a78bfa';
const iconColor = isMain ? 'text-yellow-400' : 'text-purple-400';
return (
<Surface
variant="muted"
rounded="xl"
border
padding={5}
style={{
cursor: available ? 'pointer' : 'default',
opacity: available ? 1 : 0.6,
borderColor: isSelected ? '#3b82f6' : '#262626',
boxShadow: isSelected ? '0 0 0 2px rgba(59, 130, 246, 0.2)' : 'none'
}}
onClick={available ? onClick : undefined}
>
<Stack direction="row" align="start" justify="between" mb={4}>
<Box>
<Stack direction="row" align="center" gap={2} mb={1}>
<Icon icon={TierIcon} size={5} color={iconColor} />
<Heading level={3}>{isMain ? 'Main Sponsor' : 'Secondary Sponsor'}</Heading>
</Stack>
<Text size="sm" color="text-gray-400">
{isMain ? 'Primary branding position' : 'Supporting branding position'}
</Text>
</Box>
<Badge variant={available ? 'success' : 'default'}>
{isMain
? (available ? 'Available' : 'Filled')
: (available ? `${availableCount}/${totalCount} Available` : 'Full')
}
</Badge>
</Stack>
<Box mb={4}>
<Text size="3xl" weight="bold" color="text-white">
${price}
<Text size="sm" weight="normal" color="text-gray-500">/season</Text>
</Text>
</Box>
<Stack gap={2} mb={4}>
{benefits.map((benefit, i) => (
<Stack key={i} direction="row" align="center" gap={2}>
<Icon icon={CheckCircle2} size={4} color="#10b981" />
<Text size="sm" color="text-gray-300">{benefit}</Text>
</Stack>
))}
</Stack>
{isSelected && available && (
<Box style={{ position: 'absolute', top: '1rem', right: '1rem' }}>
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: '#3b82f6', width: '1rem', height: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon icon={CheckCircle2} size={3} color="white" />
</Surface>
</Box>
)}
</Surface>
<UiSponsorTierCard
type={type}
available={available}
availableCount={availableCount}
totalCount={totalCount}
price={price}
benefits={benefits}
isSelected={isSelected}
onClick={onClick}
icon={TierIcon}
iconColor={iconColor}
/>
);
}

View File

@@ -1,25 +1,14 @@
'use client';
import { motion, useReducedMotion, AnimatePresence } from 'framer-motion';
import { useEffect, useState } from 'react';
import React from 'react';
import {
Search,
MousePointer,
CreditCard,
CheckCircle2,
Car,
Eye,
TrendingUp,
Building2
} from 'lucide-react';
interface WorkflowStep {
id: number;
icon: typeof Search;
title: string;
description: string;
color: string;
}
import { WorkflowMockup, WorkflowStep } from '@/ui/WorkflowMockup';
const WORKFLOW_STEPS: WorkflowStep[] = [
{
@@ -59,142 +48,6 @@ const WORKFLOW_STEPS: WorkflowStep[] = [
},
];
export default function SponsorWorkflowMockup() {
const shouldReduceMotion = useReducedMotion();
const [isMounted, setIsMounted] = useState(false);
const [activeStep, setActiveStep] = useState(0);
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
if (!isMounted) return;
const interval = setInterval(() => {
setActiveStep((prev) => (prev + 1) % WORKFLOW_STEPS.length);
}, 3000);
return () => clearInterval(interval);
}, [isMounted]);
if (!isMounted) {
return (
<div className="relative w-full max-w-4xl mx-auto">
<div className="bg-iron-gray/50 rounded-2xl border border-charcoal-outline p-8">
<div className="grid grid-cols-5 gap-4">
{WORKFLOW_STEPS.map((step) => (
<div key={step.id} className="flex flex-col items-center text-center">
<div className="w-14 h-14 rounded-xl bg-iron-gray border border-charcoal-outline flex items-center justify-center mb-3">
<step.icon className={`w-6 h-6 ${step.color}`} />
</div>
<h4 className="text-sm font-medium text-white mb-1">{step.title}</h4>
<p className="text-xs text-gray-500">{step.description}</p>
</div>
))}
</div>
</div>
</div>
);
}
return (
<div className="relative w-full max-w-4xl mx-auto">
<div className="bg-iron-gray/50 rounded-2xl border border-charcoal-outline p-6 sm:p-8 overflow-hidden">
{/* Connection Lines */}
<div className="absolute top-[4.5rem] left-[10%] right-[10%] hidden sm:block">
<div className="h-0.5 bg-charcoal-outline relative">
<motion.div
className="absolute h-full bg-gradient-to-r from-primary-blue to-performance-green"
initial={{ width: '0%' }}
animate={{ width: `${(activeStep / (WORKFLOW_STEPS.length - 1)) * 100}%` }}
transition={{ duration: 0.5, ease: 'easeInOut' }}
/>
</div>
</div>
{/* Steps */}
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4 relative">
{WORKFLOW_STEPS.map((step, index) => {
const isActive = index === activeStep;
const isCompleted = index < activeStep;
const StepIcon = step.icon;
return (
<motion.div
key={step.id}
className="flex flex-col items-center text-center cursor-pointer"
onClick={() => setActiveStep(index)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<motion.div
className={`w-14 h-14 rounded-xl border flex items-center justify-center mb-3 transition-all duration-300 ${
isActive
? 'bg-primary-blue/20 border-primary-blue shadow-[0_0_20px_rgba(25,140,255,0.3)]'
: isCompleted
? 'bg-performance-green/20 border-performance-green/50'
: 'bg-iron-gray border-charcoal-outline'
}`}
animate={isActive && !shouldReduceMotion ? {
scale: [1, 1.1, 1],
transition: { duration: 1, repeat: Infinity }
} : {}}
>
{isCompleted ? (
<CheckCircle2 className="w-6 h-6 text-performance-green" />
) : (
<StepIcon className={`w-6 h-6 ${isActive ? step.color : 'text-gray-500'}`} />
)}
</motion.div>
<h4 className={`text-sm font-medium mb-1 transition-colors ${
isActive ? 'text-white' : 'text-gray-400'
}`}>
{step.title}
</h4>
<p className={`text-xs transition-colors ${
isActive ? 'text-gray-300' : 'text-gray-600'
}`}>
{step.description}
</p>
</motion.div>
);
})}
</div>
{/* Active Step Preview */}
<AnimatePresence mode="wait">
<motion.div
key={activeStep}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="mt-8 pt-6 border-t border-charcoal-outline"
>
{(() => {
const currentStep = WORKFLOW_STEPS[activeStep] ?? WORKFLOW_STEPS[0];
if (!currentStep) return null;
const Icon = currentStep.icon;
return (
<div className="flex items-center justify-center gap-3">
<div className="w-8 h-8 rounded-lg bg-iron-gray flex items-center justify-center">
<Icon className={`w-4 h-4 ${currentStep.color}`} />
</div>
<div className="text-left">
<p className="text-sm text-gray-400">
Step {activeStep + 1} of {WORKFLOW_STEPS.length}
</p>
<p className="text-white font-medium">{currentStep.title}</p>
</div>
</div>
);
})()}
</motion.div>
</AnimatePresence>
</div>
</div>
);
}
export function SponsorWorkflowMockup() {
return <WorkflowMockup steps={WORKFLOW_STEPS} />;
}

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { Link } from '@/ui/Link';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
'use client';
import { LucideIcon } from 'lucide-react';
import { SponsorshipCategoryCard as UiSponsorshipCategoryCard } from '@/ui/SponsorshipCategoryCard';
interface SponsorshipCategoryCardProps {
icon: React.ElementType;
icon: LucideIcon;
title: string;
count: number;
impressions: number;
@@ -15,7 +13,7 @@ interface SponsorshipCategoryCardProps {
}
export function SponsorshipCategoryCard({
icon: Icon,
icon,
title,
count,
impressions,
@@ -23,26 +21,13 @@ export function SponsorshipCategoryCard({
href
}: SponsorshipCategoryCardProps) {
return (
<Box>
<Link href={href} variant="ghost" style={{ display: 'block' }}>
<Card style={{ padding: '1rem', transition: 'all 0.3s', cursor: 'pointer' }}>
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={3}>
<Box style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', backgroundColor: '#262626', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon style={{ width: '1.25rem', height: '1.25rem', color }} />
</Box>
<Box>
<Text weight="medium" color="text-white" style={{ display: 'block' }}>{title}</Text>
<Text size="sm" color="text-gray-500">{count} active</Text>
</Box>
</Stack>
<Box style={{ textAlign: 'right' }}>
<Text weight="semibold" color="text-white" style={{ display: 'block' }}>{impressions.toLocaleString()}</Text>
<Text size="xs" color="text-gray-500">impressions</Text>
</Box>
</Stack>
</Card>
</Link>
</Box>
<UiSponsorshipCategoryCard
icon={icon}
title={title}
count={count}
impressions={impressions}
color={color}
href={href}
/>
);
}