website refactor
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'll receive the payment minus 10% platform fee.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user