website refactor

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

View File

@@ -0,0 +1,71 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Globe, Link as LinkIcon } from 'lucide-react';
interface ConnectedAccountsPanelProps {
iracingId?: string | number;
onConnectIRacing?: () => void;
}
export function ConnectedAccountsPanel({ iracingId, onConnectIRacing }: ConnectedAccountsPanelProps) {
return (
<section aria-labelledby="connected-accounts-heading">
<Card>
<Stack gap={6}>
<Heading level={3} id="connected-accounts-heading" fontSize="1.125rem">Connected Accounts</Heading>
<Stack gap={4} className="divide-y divide-border-gray/30">
<Stack direction="row" justify="between" align="center" pt={0}>
<Stack direction="row" align="center" gap={3}>
<Box backgroundColor="#1e293b" p={2} rounded="md">
<Globe size={20} color="#4ED4E0" />
</Box>
<Stack gap={0.5}>
<Text weight="bold" size="sm">iRacing</Text>
<Text size="xs" color="#9ca3af">
{iracingId ? `Connected ID: ${iracingId}` : 'Not connected'}
</Text>
</Stack>
</Stack>
{!iracingId && (
<Button size="sm" variant="secondary" onClick={onConnectIRacing}>
Connect
</Button>
)}
{iracingId && (
<Box backgroundColor="rgba(16, 185, 129, 0.1)" px={2} py={1} rounded="full">
<Text size="xs" color="#10b981" weight="bold">Verified</Text>
</Box>
)}
</Stack>
<Stack direction="row" justify="between" align="center" pt={4}>
<Stack direction="row" align="center" gap={3}>
<Box backgroundColor="#1e293b" p={2} rounded="md">
<LinkIcon size={20} color="#198CFF" />
</Box>
<Stack gap={0.5}>
<Text weight="bold" size="sm">Discord</Text>
<Text size="xs" color="#9ca3af">Connect for notifications</Text>
</Stack>
</Stack>
<Button size="sm" variant="secondary">
Connect
</Button>
</Stack>
</Stack>
</Stack>
</Card>
</section>
);
}
import { Box } from '@/ui/Box';

View File

@@ -0,0 +1,28 @@
'use client';
import React from 'react';
import { Grid } from '@/ui/Grid';
import { LiveryCard } from '@/ui/LiveryCard';
import { ProfileSection } from './ProfileSection';
import { ProfileLiveryViewData } from '@/lib/view-data/ProfileLiveriesViewData';
interface LiveryGalleryProps {
liveries: ProfileLiveryViewData[];
action?: React.ReactNode;
}
export function LiveryGallery({ liveries, action }: LiveryGalleryProps) {
return (
<ProfileSection
title="My Liveries"
description="Manage your custom car liveries and designs."
action={action}
>
<Grid cols={3} gap={6}>
{liveries.map((livery) => (
<LiveryCard key={livery.id} livery={livery} />
))}
</Grid>
</ProfileSection>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import React from 'react';
import { Stack } from '@/ui/Stack';
import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
import { LeagueListItem } from '@/ui/LeagueListItem';
import { ProfileSection } from './ProfileSection';
interface League {
leagueId: string;
name: string;
description: string;
logoUrl?: string;
memberCount: number;
roleLabel: string;
}
interface MembershipPanelProps {
ownedLeagues: League[];
memberLeagues: League[];
}
export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanelProps) {
return (
<Stack gap={8}>
<ProfileSection
title="Leagues You Own"
description="Manage the leagues you have created and lead."
>
{ownedLeagues.length === 0 ? (
<Card>
<Text size="sm" color="text-gray-400">
You don&apos;t own any leagues yet.
</Text>
</Card>
) : (
<Stack gap={3}>
{ownedLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} isAdmin />
))}
</Stack>
)}
</ProfileSection>
<ProfileSection
title="Leagues You're In"
description="Leagues where you are a participating member."
>
{memberLeagues.length === 0 ? (
<Card>
<Text size="sm" color="text-gray-400">
You&apos;re not a member of any other leagues yet.
</Text>
</Card>
) : (
<Stack gap={3}>
{memberLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} />
))}
</Stack>
)}
</ProfileSection>
</Stack>
);
}

View File

@@ -0,0 +1,97 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Select } from '@/ui/Select';
import { Toggle } from '@/ui/Toggle';
interface PreferencesPanelProps {
preferences: {
favoriteCarClass: string;
favoriteSeries: string;
competitiveLevel: string;
showProfile: boolean;
showHistory: boolean;
};
isEditing?: boolean;
onUpdate?: (updates: Partial<PreferencesPanelProps['preferences']>) => void;
}
export function PreferencesPanel({ preferences, isEditing, onUpdate }: PreferencesPanelProps) {
if (isEditing) {
return (
<section aria-labelledby="preferences-heading">
<Card>
<Stack gap={6}>
<Heading level={3} id="preferences-heading" fontSize="1.125rem">Racing Preferences</Heading>
<Stack gap={4}>
<Select
label="Favorite Car Class"
value={preferences.favoriteCarClass}
onChange={(e) => onUpdate?.({ favoriteCarClass: e.target.value })}
options={[
{ value: 'GT3', label: 'GT3' },
{ value: 'GT4', label: 'GT4' },
{ value: 'Formula', label: 'Formula' },
{ value: 'LMP2', label: 'LMP2' },
]}
/>
<Select
label="Competitive Level"
value={preferences.competitiveLevel}
onChange={(e) => onUpdate?.({ competitiveLevel: e.target.value })}
options={[
{ value: 'casual', label: 'Casual' },
{ value: 'competitive', label: 'Competitive' },
{ value: 'professional', label: 'Professional' },
]}
/>
<Stack gap={3} pt={2}>
<Toggle
label="Public Profile"
checked={preferences.showProfile}
onChange={(checked) => onUpdate?.({ showProfile: checked })}
/>
<Toggle
label="Show Race History"
checked={preferences.showHistory}
onChange={(checked) => onUpdate?.({ showHistory: checked })}
/>
</Stack>
</Stack>
</Stack>
</Card>
</section>
);
}
return (
<section aria-labelledby="preferences-heading">
<Card>
<Stack gap={6}>
<Heading level={3} id="preferences-heading" fontSize="1.125rem">Racing Preferences</Heading>
<Stack direction="row" gap={8} wrap>
<Stack gap={1}>
<Text size="xs" color="#6b7280" weight="bold" letterSpacing="0.05em" uppercase>Car Class</Text>
<Text color="#d1d5db">{preferences.favoriteCarClass}</Text>
</Stack>
<Stack gap={1}>
<Text size="xs" color="#6b7280" weight="bold" letterSpacing="0.05em" uppercase>Level</Text>
<Text color="#d1d5db" capitalize>{preferences.competitiveLevel}</Text>
</Stack>
<Stack gap={1}>
<Text size="xs" color="#6b7280" weight="bold" letterSpacing="0.05em" uppercase>Visibility</Text>
<Text color="#d1d5db">{preferences.showProfile ? 'Public' : 'Private'}</Text>
</Stack>
</Stack>
</Stack>
</Card>
</section>
);
}

View File

@@ -0,0 +1,81 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Input } from '@/ui/Input';
import { TextArea } from '@/ui/TextArea';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
interface ProfileDetailsPanelProps {
driver: {
name: string;
country: string;
bio?: string | null;
};
isEditing?: boolean;
onUpdate?: (updates: { bio?: string; country?: string }) => void;
}
export function ProfileDetailsPanel({ driver, isEditing, onUpdate }: ProfileDetailsPanelProps) {
if (isEditing) {
return (
<section aria-labelledby="profile-details-heading">
<Card>
<Stack gap={6}>
<Heading level={3} id="profile-details-heading" fontSize="1.125rem">Profile Details</Heading>
<Stack gap={4}>
<Input
label="Nationality (ISO Code)"
value={driver.country}
onChange={(e) => onUpdate?.({ country: e.target.value })}
placeholder="e.g. US, GB, DE"
maxLength={2}
/>
<TextArea
label="Bio"
value={driver.bio || ''}
onChange={(e) => onUpdate?.({ bio: e.target.value })}
placeholder="Tell the community about your racing career..."
rows={4}
/>
</Stack>
</Stack>
</Card>
</section>
);
}
return (
<section aria-labelledby="profile-details-heading">
<Card>
<Stack gap={6}>
<Stack direction="row" justify="between" align="center">
<Heading level={3} id="profile-details-heading" fontSize="1.125rem">Profile Details</Heading>
</Stack>
<Stack gap={4}>
<Stack gap={1}>
<Text size="xs" color="#6b7280" weight="bold" letterSpacing="0.05em" uppercase>Nationality</Text>
<Stack direction="row" align="center" gap={2}>
<Text size="xl">
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
<Text color="#d1d5db">{driver.country}</Text>
</Stack>
</Stack>
<Stack gap={1}>
<Text size="xs" color="#6b7280" weight="bold" letterSpacing="0.05em" uppercase>Bio</Text>
<Text color="#d1d5db" lineHeight="relaxed">
{driver.bio || 'No bio provided.'}
</Text>
</Stack>
</Stack>
</Stack>
</Card>
</section>
);
}

View File

@@ -0,0 +1,140 @@
'use client';
import React from 'react';
import { mediaConfig } from '@/lib/config/mediaConfig';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Calendar, Globe, Star, Trophy, UserPlus } from 'lucide-react';
interface ProfileHeaderProps {
driver: {
name: string;
avatarUrl?: string;
country: string;
iracingId: number;
joinedAt: string | Date;
};
stats: {
rating: number;
} | null;
globalRank: number;
onAddFriend?: () => void;
friendRequestSent?: boolean;
isOwnProfile?: boolean;
}
export function ProfileHeader({
driver,
stats,
globalRank,
onAddFriend,
friendRequestSent,
isOwnProfile,
}: ProfileHeaderProps) {
return (
<header>
<Surface variant="muted" rounded="xl" border padding={6} backgroundColor="#141619" borderColor="#23272B">
<Stack direction="row" align="center" gap={8} wrap>
{/* Avatar with telemetry-style border */}
<Box position="relative">
<Box
width="6rem"
height="6rem"
rounded="md"
border
borderColor="#23272B"
p={0.5}
backgroundColor="#0C0D0F"
>
<Box width="100%" height="100%" rounded="sm" overflow="hidden">
<Image
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
alt={driver.name}
width={96}
height={96}
objectFit="cover"
fullWidth
fullHeight
/>
</Box>
</Box>
</Box>
{/* Driver Info */}
<Box flexGrow={1} minWidth="0">
<Stack direction="row" align="center" gap={3} mb={1}>
<Heading level={1} fontSize="1.5rem">
{driver.name}
</Heading>
<Text size="2xl" aria-label={`Country: ${driver.country}`}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
</Stack>
<Stack direction="row" align="center" gap={4} color="#9ca3af">
<Stack direction="row" align="center" gap={1.5}>
<Globe size={14} />
<Text size="xs" font="mono">ID: {driver.iracingId}</Text>
</Stack>
<Box width="1px" height="12px" backgroundColor="#23272B" />
<Stack direction="row" align="center" gap={1.5}>
<Calendar size={14} />
<Text size="xs">
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Text>
</Stack>
</Stack>
</Box>
{/* Stats Grid */}
<Stack direction="row" align="center" gap={6}>
{stats && (
<>
<Box>
<Stack gap={0.5}>
<Text size="xs" color="#6b7280" weight="bold" letterSpacing="0.05em">RATING</Text>
<Stack direction="row" align="center" gap={1.5}>
<Star size={14} color="#198CFF" />
<Text font="mono" size="lg" weight="bold" color="#198CFF">{stats.rating}</Text>
</Stack>
</Stack>
</Box>
<Box width="1px" height="32px" backgroundColor="#23272B" />
<Box>
<Stack gap={0.5}>
<Text size="xs" color="#6b7280" weight="bold" letterSpacing="0.05em">GLOBAL RANK</Text>
<Stack direction="row" align="center" gap={1.5}>
<Trophy size={14} color="#FFBE4D" />
<Text font="mono" size="lg" weight="bold" color="#FFBE4D">#{globalRank}</Text>
</Stack>
</Stack>
</Box>
</>
)}
</Stack>
{/* Actions */}
{!isOwnProfile && onAddFriend && (
<Box ml="auto">
<Button
variant={friendRequestSent ? 'secondary' : 'primary'}
onClick={onAddFriend}
disabled={friendRequestSent}
size="sm"
icon={<UserPlus size={16} />}
>
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
</Box>
)}
</Stack>
</Surface>
</header>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
export type ProfileTab = 'overview' | 'history' | 'stats' | 'leagues' | 'liveries' | 'settings';
interface ProfileNavTabsProps {
activeTab: ProfileTab;
onTabChange: (tab: ProfileTab) => void;
tabs?: { id: ProfileTab; label: string }[];
}
const DEFAULT_TABS: { id: ProfileTab; label: string }[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'history', label: 'History' },
{ id: 'stats', label: 'Stats' },
{ id: 'leagues', label: 'Leagues' },
{ id: 'liveries', label: 'Liveries' },
{ id: 'settings', label: 'Settings' },
];
export function ProfileNavTabs({ activeTab, onTabChange, tabs = DEFAULT_TABS }: ProfileNavTabsProps) {
return (
<nav aria-label="Profile navigation">
<Box borderBottom borderColor="#23272B">
<Stack direction="row" gap={8}>
{tabs.map((tab) => (
<Box
key={tab.id}
onClick={() => onTabChange(tab.id)}
pb={4}
cursor="pointer"
borderBottom
borderColor={activeTab === tab.id ? '#198CFF' : 'transparent'}
color={activeTab === tab.id ? '#198CFF' : '#9ca3af'}
transition
fontSize="0.875rem"
fontWeight={activeTab === tab.id ? '600' : '400'}
mb="-1px"
position="relative"
className="group"
>
<Text capitalize>{tab.label}</Text>
{activeTab === tab.id && (
<Box
position="absolute"
bottom={0}
left={0}
right={0}
height="2px"
backgroundColor="#198CFF"
boxShadow="0 0 8px rgba(25, 140, 255, 0.5)"
/>
)}
</Box>
))}
</Stack>
</Box>
</nav>
);
}

View File

@@ -0,0 +1,33 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface ProfileSectionProps {
title: string;
description?: string;
action?: React.ReactNode;
children: React.ReactNode;
}
export function ProfileSection({ title, description, action, children }: ProfileSectionProps) {
return (
<Box mb={8}>
<Stack direction="row" align="center" justify="between" mb={4}>
<Box>
<Heading level={2}>{title}</Heading>
{description && (
<Text color="text-gray-400" size="sm" mt={1} block>
{description}
</Text>
)}
</Box>
{action && <Box>{action}</Box>}
</Stack>
<Box>{children}</Box>
</Box>
);
}

View File

@@ -0,0 +1,168 @@
'use client';
import React, { useState } from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select';
import { Toggle } from '@/ui/Toggle';
import { TextArea } from '@/ui/TextArea';
import { Checkbox } from '@/ui/Checkbox';
import { ProfileSection } from './ProfileSection';
interface ProfileSettingsPanelProps {
driver: {
id: string;
name: string;
country: string;
bio?: string | null;
};
onSave?: (updates: { bio?: string; country?: string }) => void;
}
export function ProfileSettingsPanel({ driver, onSave }: ProfileSettingsPanelProps) {
const [bio, setBio] = useState(driver.bio || '');
const [nationality, setNationality] = useState(driver.country);
const [favoriteCarClass, setFavoriteCarClass] = useState('GT3');
const [favoriteSeries, setFavoriteSeries] = useState('Endurance');
const [competitiveLevel, setCompetitiveLevel] = useState('competitive');
const [preferredRegions, setPreferredRegions] = useState<string[]>(['EU']);
const handleSave = () => {
onSave?.({
bio,
country: nationality
});
};
return (
<Stack gap={8}>
<ProfileSection
title="Profile Information"
description="Update your personal details and driver bio."
>
<Card>
<Stack gap={4}>
<TextArea
label="Bio"
value={bio}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setBio(e.target.value)}
rows={4}
placeholder="Tell us about yourself..."
/>
<Input
label="Nationality"
type="text"
value={nationality}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNationality(e.target.value)}
placeholder="e.g., US, GB, DE"
maxLength={2}
/>
</Stack>
</Card>
</ProfileSection>
<ProfileSection
title="Racing Preferences"
description="Help us tailor your experience by sharing your racing style."
>
<Card>
<Stack gap={4}>
<Select
label="Favorite Car Class"
value={favoriteCarClass}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFavoriteCarClass(e.target.value)}
options={[
{ value: 'GT3', label: 'GT3' },
{ value: 'GT4', label: 'GT4' },
{ value: 'Formula', label: 'Formula' },
{ value: 'LMP2', label: 'LMP2' },
{ value: 'Touring', label: 'Touring Cars' },
{ value: 'NASCAR', label: 'NASCAR' },
]}
/>
<Select
label="Favorite Series Type"
value={favoriteSeries}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFavoriteSeries(e.target.value)}
options={[
{ value: 'Sprint', label: 'Sprint' },
{ value: 'Endurance', label: 'Endurance' },
{ value: 'Mixed', label: 'Mixed' },
]}
/>
<Select
label="Competitive Level"
value={competitiveLevel}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setCompetitiveLevel(e.target.value)}
options={[
{ value: 'casual', label: 'Casual - Just for fun' },
{ value: 'competitive', label: 'Competitive - Aiming to win' },
{ value: 'professional', label: 'Professional - Esports focused' },
]}
/>
<Stack gap={2}>
<Heading level={4}>Preferred Regions</Heading>
<Stack direction="row" gap={4}>
{['NA', 'EU', 'ASIA', 'OCE'].map(region => (
<Checkbox
key={region}
label={region}
checked={preferredRegions.includes(region)}
onChange={(checked) => {
if (checked) {
setPreferredRegions([...preferredRegions, region]);
} else {
setPreferredRegions(preferredRegions.filter(r => r !== region));
}
}}
/>
))}
</Stack>
</Stack>
</Stack>
</Card>
</ProfileSection>
<ProfileSection
title="Privacy Settings"
description="Control who can see your profile and activity."
>
<Card>
<Stack gap={2}>
<Toggle
checked={true}
onChange={() => {}}
label="Show profile to other drivers"
/>
<Toggle
checked={true}
onChange={() => {}}
label="Show race history"
/>
<Toggle
checked={true}
onChange={() => {}}
label="Allow friend requests"
/>
</Stack>
</Card>
</ProfileSection>
<Stack direction="row" gap={3} justify="end">
<Button variant="secondary">
Cancel
</Button>
<Button variant="primary" onClick={handleSave}>
Save Changes
</Button>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,87 @@
'use client';
import React from 'react';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { Stack } from '@/ui/Stack';
import { Trophy, Calendar } from 'lucide-react';
interface SessionResult {
id: string;
date: string;
event: string;
car: string;
position: number;
fieldSize: number;
ratingChange: number;
}
interface SessionHistoryTableProps {
results: SessionResult[];
}
export function SessionHistoryTable({ results }: SessionHistoryTableProps) {
if (results.length === 0) {
return (
<Stack align="center" justify="center" py={12} gap={4}>
<Trophy size={48} color="#23272B" />
<Text color="#6b7280">No race history found.</Text>
</Stack>
);
}
return (
<Table>
<TableHead>
<TableRow>
<TableHeader>Date</TableHeader>
<TableHeader>Event</TableHeader>
<TableHeader>Car</TableHeader>
<TableHeader>Pos</TableHeader>
<TableHeader className="text-right">Rating</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{results.map((result) => (
<TableRow key={result.id}>
<TableCell>
<Stack direction="row" align="center" gap={2}>
<Calendar size={12} className="text-gray-500" />
<Text size="xs" font="mono">
{new Date(result.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</Text>
</Stack>
</TableCell>
<TableCell>
<Text weight="bold" size="sm">{result.event}</Text>
</TableCell>
<TableCell>
<Text size="xs" color="#9ca3af">{result.car}</Text>
</TableCell>
<TableCell>
<Stack direction="row" align="center" gap={2}>
<Badge
variant={result.position === 1 ? 'success' : result.position <= 3 ? 'warning' : 'default'}
>
P{result.position}
</Badge>
<Text size="xs" color="#6b7280">/ {result.fieldSize}</Text>
</Stack>
</TableCell>
<TableCell className="text-right">
<Text
size="xs"
font="mono"
weight="bold"
color={result.ratingChange >= 0 ? '#10b981' : '#ef4444'}
>
{result.ratingChange >= 0 ? '+' : ''}{result.ratingChange}
</Text>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,101 @@
'use client';
import React from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { ProfileSection } from './ProfileSection';
interface Request {
id: string;
sponsorName: string;
message?: string | null;
createdAtIso: string;
}
interface Section {
entityId: string;
entityName: string;
entityType: string;
requests: Request[];
}
interface SponsorshipRequestsPanelProps {
sections: Section[];
onAccept: (requestId: string) => Promise<void>;
onReject: (requestId: string, reason?: string) => Promise<void>;
processingId?: string | null;
}
export function SponsorshipRequestsPanel({
sections,
onAccept,
onReject,
processingId,
}: SponsorshipRequestsPanelProps) {
return (
<Stack gap={8}>
{sections.map((section) => (
<ProfileSection
key={`${section.entityType}-${section.entityId}`}
title={section.entityName}
description={`Manage pending sponsorship requests for ${section.entityName}.`}
>
<Card>
{section.requests.length === 0 ? (
<Text size="sm" color="text-gray-400">No pending requests.</Text>
) : (
<Stack gap={3}>
{section.requests.map((request) => (
<Surface
key={request.id}
variant="muted"
rounded="lg"
border
padding={4}
backgroundColor="#0C0D0F"
borderColor="#23272B"
>
<Stack direction="row" align="center" justify="between" wrap gap={4}>
<Box flexGrow={1} minWidth="0">
<Text weight="medium" color="text-white" block>{request.sponsorName}</Text>
{request.message && (
<Text size="xs" color="text-gray-400" block mt={1}>{request.message}</Text>
)}
<Text size="xs" color="text-gray-500" block mt={2}>
{DateDisplay.formatShort(request.createdAtIso)}
</Text>
</Box>
<Stack direction="row" gap={2}>
<Button
variant="primary"
onClick={() => onAccept(request.id)}
size="sm"
disabled={!!processingId}
>
{processingId === request.id ? 'Accepting...' : 'Accept'}
</Button>
<Button
variant="secondary"
onClick={() => onReject(request.id)}
size="sm"
disabled={!!processingId}
>
{processingId === request.id ? 'Rejecting...' : 'Reject'}
</Button>
</Stack>
</Stack>
</Surface>
))}
</Stack>
)}
</Card>
</ProfileSection>
))}
</Stack>
);
}