website refactor

This commit is contained in:
2026-01-18 22:55:55 +01:00
parent b43a23a48c
commit aeaa43f4d3
179 changed files with 4736 additions and 6832 deletions

View File

@@ -1,11 +1,11 @@
'use client';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { Panel } from '@/ui/Panel';
import { AccountItem } from '@/ui/AccountItem';
import { Badge } from '@/ui/Badge';
import { Globe, Link as LinkIcon } from 'lucide-react';
import React from 'react';
interface ConnectedAccountsPanelProps {
iracingId?: string | number;
@@ -14,56 +14,35 @@ interface ConnectedAccountsPanelProps {
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}>
<Stack backgroundColor="#1e293b" p={2} rounded="md">
<Globe size={20} color="#4ED4E0" />
</Stack>
<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 && (
<Stack backgroundColor="rgba(16, 185, 129, 0.1)" px={2} py={1} rounded="full">
<Text size="xs" color="#10b981" weight="bold">Verified</Text>
</Stack>
)}
</Stack>
<Panel title="Connected Accounts">
<AccountItem
icon={Globe}
title="iRacing"
description={iracingId ? `Connected ID: ${iracingId}` : 'Not connected'}
intent="telemetry"
action={
iracingId ? (
<Badge variant="success">Verified</Badge>
) : (
<Button size="sm" variant="secondary" onClick={onConnectIRacing}>
Connect
</Button>
)
}
/>
<Stack direction="row" justify="between" align="center" pt={4}>
<Stack direction="row" align="center" gap={3}>
<Stack backgroundColor="#1e293b" p={2} rounded="md">
<LinkIcon size={20} color="#198CFF" />
</Stack>
<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>
<AccountItem
icon={LinkIcon}
title="Discord"
description="Connect for notifications"
intent="primary"
action={
<Button size="sm" variant="secondary">
Connect
</Button>
}
/>
</Panel>
);
}

View File

@@ -2,9 +2,9 @@
import { LeagueListItem } from '@/components/leagues/LeagueListItem';
import { Card } from '@/ui/Card';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { ProfileSection } from './ProfileSection';
import React from 'react';
interface League {
leagueId: string;
@@ -22,23 +22,23 @@ interface MembershipPanelProps {
export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanelProps) {
return (
<Stack gap={8}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<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">
<Text size="sm" variant="low">
You don&apos;t own any leagues yet.
</Text>
</Card>
) : (
<Stack gap={3}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{ownedLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} isAdmin />
))}
</Stack>
</div>
)}
</ProfileSection>
@@ -48,18 +48,18 @@ export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanel
>
{memberLeagues.length === 0 ? (
<Card>
<Text size="sm" color="text-gray-400">
<Text size="sm" variant="low">
You&apos;re not a member of any other leagues yet.
</Text>
</Card>
) : (
<Stack gap={3}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{memberLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} />
))}
</Stack>
</div>
)}
</ProfileSection>
</Stack>
</div>
);
}

View File

@@ -1,11 +1,10 @@
'use client';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/primitives/Stack';
import { Panel } from '@/ui/Panel';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { Toggle } from '@/ui/Toggle';
import { ProfileStatsGroup, ProfileStat } from '@/ui/ProfileHero';
import React from 'react';
interface PreferencesPanelProps {
preferences: {
@@ -22,75 +21,54 @@ interface PreferencesPanelProps {
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>
<Panel title="Racing Preferences">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<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' },
]}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', paddingTop: '0.5rem' }}>
<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 })}
/>
</div>
</div>
</Panel>
);
}
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>
<Panel title="Racing Preferences">
<ProfileStatsGroup>
<ProfileStat label="Car Class" value={preferences.favoriteCarClass} intent="primary" />
<ProfileStat label="Level" value={preferences.competitiveLevel} intent="telemetry" />
<ProfileStat label="Visibility" value={preferences.showProfile ? 'Public' : 'Private'} intent="low" />
</ProfileStatsGroup>
</Panel>
);
}

View File

@@ -1,11 +1,6 @@
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/primitives/Box';
import { Panel } from '@/ui/Panel';
import { Text } from '@/ui/Text';
import { User } from 'lucide-react';
import React from 'react';
interface ProfileBioProps {
bio: string;
@@ -13,13 +8,10 @@ interface ProfileBioProps {
export function ProfileBio({ bio }: ProfileBioProps) {
return (
<Card>
<Box mb={3}>
<Heading level={2} icon={<Icon icon={User} size={5} color="#3b82f6" />}>
About
</Heading>
</Box>
<Text color="text-gray-300">{bio}</Text>
</Card>
<Panel
title="About"
>
<Text variant="med">{bio}</Text>
</Panel>
);
}

View File

@@ -1,12 +1,12 @@
'use client';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Panel } from '@/ui/Panel';
import { Input } from '@/ui/Input';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
import { ProfileStat } from '@/ui/ProfileHero';
import React from 'react';
interface ProfileDetailsPanelProps {
driver: {
@@ -21,60 +21,45 @@ interface ProfileDetailsPanelProps {
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>
<Panel title="Profile Details">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Input
label="Nationality (ISO Code)"
value={driver.country}
onChange={(e) => onUpdate?.({ country: e.target.value })}
placeholder="e.g. US, GB, DE"
/>
<TextArea
label="Bio"
value={driver.bio || ''}
onChange={(e) => onUpdate?.({ bio: e.target.value })}
placeholder="Tell the community about your racing career..."
/>
</div>
</Panel>
);
}
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>
<Panel title="Profile Details">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<Text size="xs" variant="low" weight="bold" uppercase block marginBottom={1}>Nationality</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Text size="xl">
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
<Text variant="med">{driver.country}</Text>
</div>
</div>
<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>
<div>
<Text size="xs" variant="low" weight="bold" uppercase block marginBottom={1}>Bio</Text>
<Text variant="med" leading="relaxed">
{driver.bio || 'No bio provided.'}
</Text>
</div>
</div>
</Panel>
);
}

View File

@@ -5,10 +5,10 @@ import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/primitives/Stack';
import { Surface } from '@/ui/primitives/Surface';
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '@/ui/ProfileHero';
import { Text } from '@/ui/Text';
import { Calendar, Globe, Star, Trophy, UserPlus } from 'lucide-react';
import React from 'react';
interface ProfileHeaderProps {
driver: {
@@ -36,103 +36,63 @@ export function ProfileHeader({
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 */}
<Stack position="relative">
<Stack
width="6rem"
height="6rem"
rounded="md"
border
borderColor="#23272B"
p={0.5}
backgroundColor="#0C0D0F"
>
<Stack 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
/>
</Stack>
</Stack>
</Stack>
<ProfileHero variant="muted">
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem', flexWrap: 'wrap' }}>
<ProfileAvatar>
<Image
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
alt={driver.name}
width={96}
height={96}
objectFit="cover"
/>
</ProfileAvatar>
{/* Driver Info */}
<Stack 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()}
<div style={{ flex: 1, minWidth: '200px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.25rem' }}>
<Heading level={1}>{driver.name}</Heading>
<Text size="2xl" aria-label={`Country: ${driver.country}`}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<Globe size={14} color="var(--ui-color-text-low)" />
<Text size="xs" font="mono" variant="low">ID: {driver.iracingId}</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<Calendar size={14} color="var(--ui-color-text-low)" />
<Text size="xs" variant="low">
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Text>
</Stack>
</div>
</div>
</div>
<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>
<Stack 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>
</Stack>
{/* Stats Grid */}
<Stack direction="row" align="center" gap={6}>
{stats && (
<>
<Stack>
<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>
</Stack>
<Stack width="1px" height="32px" backgroundColor="#23272B" />
<Stack>
<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>
</Stack>
</>
)}
</Stack>
{/* Actions */}
{!isOwnProfile && onAddFriend && (
<Stack ml="auto">
<Button
variant={friendRequestSent ? 'secondary' : 'primary'}
onClick={onAddFriend}
disabled={friendRequestSent}
size="sm"
icon={<UserPlus size={16} />}
>
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
</Stack>
<ProfileStatsGroup>
{stats && (
<React.Fragment>
<ProfileStat label="RATING" value={stats.rating} intent="primary" />
<ProfileStat label="GLOBAL RANK" value={`#${globalRank}`} intent="warning" />
</React.Fragment>
)}
</Stack>
</Surface>
</header>
</ProfileStatsGroup>
{!isOwnProfile && onAddFriend && (
<div style={{ marginLeft: 'auto' }}>
<Button
variant={friendRequestSent ? 'secondary' : 'primary'}
onClick={onAddFriend}
disabled={friendRequestSent}
size="sm"
icon={<UserPlus size={16} />}
>
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
</div>
)}
</div>
</ProfileHero>
);
}

View File

@@ -1,8 +1,6 @@
'use client';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { SectionHeader } from '@/ui/SectionHeader';
import React from 'react';
interface ProfileSectionProps {
@@ -14,19 +12,14 @@ interface ProfileSectionProps {
export function ProfileSection({ title, description, action, children }: ProfileSectionProps) {
return (
<Stack mb={8}>
<Stack direction="row" align="center" justify="between" mb={4}>
<Stack>
<Heading level={2}>{title}</Heading>
{description && (
<Text color="text-gray-400" size="sm" mt={1} block>
{description}
</Text>
)}
</Stack>
{action && <Stack>{action}</Stack>}
</Stack>
<Stack>{children}</Stack>
</Stack>
<section style={{ marginBottom: '2rem' }}>
<SectionHeader
title={title}
description={description}
actions={action}
variant="minimal"
/>
<div>{children}</div>
</section>
);
}

View File

@@ -1,11 +1,11 @@
import { Box } from '../../ui/primitives/Box';
import { Grid } from '../../ui/primitives/Grid';
import { Text } from '../../ui/Text';
import { StatGrid } from '../../ui/StatGrid';
import { Bug } from 'lucide-react';
import React from 'react';
interface Stat {
label: string;
value: string | number;
color?: string;
intent?: 'primary' | 'telemetry' | 'success' | 'critical';
}
interface ProfileStatGridProps {
@@ -13,22 +13,18 @@ interface ProfileStatGridProps {
}
export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
const mappedStats = stats.map(stat => ({
label: stat.label,
value: stat.value,
intent: stat.intent || 'primary',
icon: Bug // Default icon if none provided, but StatBox requires one
}));
return (
<Grid cols={2} mdCols={4} gap={4}>
{stats.map((stat, idx) => (
<Box
key={idx}
p={4}
bg="bg-[#0f1115]"
rounded="xl"
border
borderColor="border-[#262626]"
textAlign="center"
>
<Text size="3xl" weight="bold" color={stat.color} block mb={1}>{stat.value}</Text>
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em">{stat.label}</Text>
</Box>
))}
</Grid>
<StatGrid
stats={mappedStats}
columns={{ base: 2, md: 4 }}
variant="box"
/>
);
}

View File

@@ -1,10 +1,9 @@
'use client';
import { Button } from '@/ui/Button';
import { SegmentedControl } from '@/ui/SegmentedControl';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/primitives/Box';
import { Surface } from '@/ui/primitives/Surface';
import { BarChart3, TrendingUp, User } from 'lucide-react';
import React from 'react';
export type ProfileTab = 'overview' | 'stats' | 'ratings';
@@ -14,34 +13,17 @@ interface ProfileTabsProps {
}
export function ProfileTabs({ activeTab, onTabChange }: ProfileTabsProps) {
const options = [
{ id: 'overview', label: 'Overview', icon: <Icon icon={User} size={4} /> },
{ id: 'stats', label: 'Detailed Stats', icon: <Icon icon={BarChart3} size={4} /> },
{ id: 'ratings', label: 'Ratings', icon: <Icon icon={TrendingUp} size={4} /> },
];
return (
<Surface variant="muted" rounded="xl" padding={1} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', border: '1px solid #262626', width: 'fit-content' }}>
<Box style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Button
variant={activeTab === 'overview' ? 'primary' : 'ghost'}
onClick={() => onTabChange('overview')}
size="sm"
icon={<Icon icon={User} size={4} />}
>
Overview
</Button>
<Button
variant={activeTab === 'stats' ? 'primary' : 'ghost'}
onClick={() => onTabChange('stats')}
size="sm"
icon={<Icon icon={BarChart3} size={4} />}
>
Detailed Stats
</Button>
<Button
variant={activeTab === 'ratings' ? 'primary' : 'ghost'}
onClick={() => onTabChange('ratings')}
size="sm"
icon={<Icon icon={TrendingUp} size={4} />}
>
Ratings
</Button>
</Box>
</Surface>
<SegmentedControl
options={options}
activeId={activeTab}
onChange={(id) => onTabChange(id as ProfileTab)}
/>
);
}

View File

@@ -8,8 +8,8 @@ import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/Drive
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link';
import { Box } from '@/ui/primitives/Box';
import { Text } from '@/ui/Text';
import { UserDropdown, UserDropdownHeader, UserDropdownItem, UserDropdownFooter } from '@/ui/UserDropdown';
import { AnimatePresence, motion } from 'framer-motion';
import {
BarChart3,
@@ -22,7 +22,7 @@ import {
Settings,
Shield
} from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
// Hook to detect demo user mode based on session
function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } {
@@ -143,32 +143,20 @@ export function UserPill() {
// Handle unauthenticated users
if (!session) {
return (
<Box display="flex" alignItems="center" gap={2}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Link
href={routes.auth.login}
variant="secondary"
rounded="full"
px={4}
py={1.5}
size="xs"
hoverTextColor="text-white"
hoverBorderColor="border-gray-500"
>
Sign In
</Link>
<Link
href={routes.auth.signup}
variant="primary"
rounded="full"
px={4}
py={1.5}
size="xs"
shadow="0 0 12px rgba(25,140,255,0.5)"
hoverBg="rgba(25,140,255,0.9)"
>
Get Started
</Link>
</Box>
</div>
);
}
@@ -186,292 +174,123 @@ export function UserPill() {
'super-admin': 'Super Admin',
} as Record<string, string>)[demoRole || 'driver'] : null;
const roleColor = isDemo ? ({
'driver': 'text-primary-blue',
'sponsor': 'text-performance-green',
'league-owner': 'text-purple-400',
'league-steward': 'text-warning-amber',
'league-admin': 'text-red-400',
'system-owner': 'text-indigo-400',
'super-admin': 'text-pink-400',
} as Record<string, string>)[demoRole || 'driver'] : null;
const roleIntent = isDemo ? ({
'driver': 'primary',
'sponsor': 'success',
'league-owner': 'primary',
'league-steward': 'warning',
'league-admin': 'critical',
'system-owner': 'primary',
'super-admin': 'primary',
} as Record<string, 'primary' | 'success' | 'warning' | 'critical'>)[demoRole || 'driver'] : 'low';
return (
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
<Box
as="button"
<div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }} data-user-pill>
<button
type="button"
onClick={() => setIsMenuOpen((open) => !open)}
display="flex"
alignItems="center"
gap={3}
rounded="full"
border
px={3}
py={1.5}
transition
cursor="pointer"
bg="linear-gradient(to r, var(--iron-gray), var(--deep-graphite))"
borderColor={isMenuOpen ? 'border-primary-blue/50' : 'border-charcoal-outline'}
transform={isMenuOpen ? 'scale(1.02)' : 'scale(1)'}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
borderRadius: '9999px',
border: `1px solid ${isMenuOpen ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-border-default)'}`,
padding: '0.375rem 0.75rem',
background: 'var(--ui-color-bg-surface-muted)',
cursor: 'pointer'
}}
>
{/* Avatar */}
<Box position="relative">
<div style={{ position: 'relative' }}>
{avatarUrl ? (
<Box w="8" h="8" rounded="full" overflow="hidden" bg="bg-charcoal-outline" display="flex" alignItems="center" justifyContent="center" border borderColor="border-charcoal-outline/80">
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
fill
/>
</Box>
</div>
) : (
<Box w="8" h="8" rounded="full" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" color="text-primary-blue">
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</Box>
</div>
)}
<Box position="absolute" bottom="-0.5" right="-0.5" w="3" h="3" rounded="full" bg="bg-primary-blue" border borderColor="border-deep-graphite" borderWidth="2px" />
</Box>
</div>
{/* Info */}
<Box display={{ base: 'none', sm: 'flex' }} flexDirection="col" alignItems="start">
<Text size="xs" weight="semibold" color="text-white" truncate maxWidth="100px" block>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'start' }}>
<Text size="xs" weight="semibold" variant="high" truncate style={{ maxWidth: '100px' }}>
{displayName}
</Text>
{roleLabel && (
<Text size="xs" color={roleColor || 'text-gray-400'} weight="medium" fontSize="10px">
<Text size="xs" variant={roleIntent as any} weight="medium" style={{ fontSize: '10px' }}>
{roleLabel}
</Text>
)}
</Box>
</div>
{/* Chevron */}
<Icon icon={ChevronDown} size={3.5} color="rgb(107, 114, 128)" groupHoverTextColor="text-gray-300" />
</Box>
<Icon icon={ChevronDown} size={3.5} intent="low" />
</button>
<AnimatePresence>
{isMenuOpen && (
<Box
as={motion.div}
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15 }}
position="absolute"
right={0}
top="100%"
mt={2}
zIndex={50}
>
<Box w="56" rounded="xl" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" shadow="xl" overflow="hidden">
{/* Header */}
<Box p={4} borderBottom borderColor="border-charcoal-outline" bg={`linear-gradient(to r, ${isDemo ? 'rgba(59, 130, 246, 0.1)' : 'rgba(38, 38, 38, 0.2)'}, transparent)`}>
<Box display="flex" alignItems="center" gap={3}>
{avatarUrl ? (
<Box w="10" h="10" rounded="lg" overflow="hidden" bg="bg-charcoal-outline" display="flex" alignItems="center" justifyContent="center" border borderColor="border-charcoal-outline/80">
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
fill
/>
</Box>
) : (
<Box w="10" h="10" rounded="lg" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" color="text-primary-blue">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</Box>
)}
<Box>
<Text size="sm" weight="semibold" color="text-white" block>{displayName}</Text>
{roleLabel && (
<Text size="xs" color={roleColor || 'text-gray-400'} block>{roleLabel}</Text>
)}
{isDemo && (
<Text size="xs" color="text-gray-500" block>Demo Account</Text>
)}
</Box>
</Box>
{isDemo && (
<Text size="xs" color="text-gray-500" block mt={2}>
Development account - not for production use
</Text>
)}
</Box>
<UserDropdown isOpen={isMenuOpen}>
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
{avatarUrl ? (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
/>
</div>
) : (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</div>
)}
<div>
<Text size="sm" weight="semibold" variant="high" block>{displayName}</Text>
{roleLabel && (
<Text size="xs" variant="low" block>{roleLabel}</Text>
)}
</div>
</div>
</UserDropdownHeader>
{/* Menu Items */}
<Box py={1}>
{/* Admin link for Owner/Super Admin users */}
{hasAdminAccess && (
<Link
href="/admin"
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Shield} size={4} color="rgb(129, 140, 248)" mr={3} />
<Text size="sm" color="text-gray-200">Admin Area</Text>
</Link>
)}
{/* Sponsor portal link for demo sponsor users */}
{isDemo && demoRole === 'sponsor' && (
<>
<Link
href="/sponsor"
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={BarChart3} size={4} color="rgb(16, 185, 129)" mr={3} />
<Text size="sm" color="text-gray-200">Dashboard</Text>
</Link>
<Link
href={routes.sponsor.campaigns}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Megaphone} size={4} color="rgb(59, 130, 246)" mr={3} />
<Text size="sm" color="text-gray-200">My Sponsorships</Text>
</Link>
<Link
href={routes.sponsor.billing}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={CreditCard} size={4} color="rgb(245, 158, 11)" mr={3} />
<Text size="sm" color="text-gray-200">Billing</Text>
</Link>
<Link
href={routes.sponsor.settings}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Settings} size={4} color="rgb(156, 163, 175)" mr={3} />
<Text size="sm" color="text-gray-200">Settings</Text>
</Link>
</>
)}
<div style={{ padding: '0.25rem 0' }}>
{hasAdminAccess && (
<UserDropdownItem href="/admin" icon={Shield} label="Admin Area" intent="primary" onClick={() => setIsMenuOpen(false)} />
)}
{isDemo && demoRole === 'sponsor' && (
<React.Fragment>
<UserDropdownItem href="/sponsor" icon={BarChart3} label="Dashboard" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.sponsor.campaigns} icon={Megaphone} label="My Sponsorships" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.sponsor.billing} icon={CreditCard} label="Billing" onClick={() => setIsMenuOpen(false)} />
</React.Fragment>
)}
{/* Regular user profile links */}
<Link
href={routes.protected.profile}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Text size="sm" color="text-gray-200">Profile</Text>
</Link>
<Link
href={routes.protected.profileLeagues}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Text size="sm" color="text-gray-200">Manage leagues</Text>
</Link>
<Link
href={routes.protected.profileLiveries}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Paintbrush} size={4} mr={2} />
<Text size="sm" color="text-gray-200">Liveries</Text>
</Link>
<Link
href={routes.protected.profileSponsorshipRequests}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Handshake} size={4} color="rgb(16, 185, 129)" mr={2} />
<Text size="sm" color="text-gray-200">Sponsorship Requests</Text>
</Link>
<Link
href={routes.protected.profileSettings}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Settings} size={4} mr={2} />
<Text size="sm" color="text-gray-200">Settings</Text>
</Link>
<UserDropdownItem href={routes.protected.profile} label="Profile" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileLeagues} label="Manage leagues" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileLiveries} icon={Paintbrush} label="Liveries" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileSponsorshipRequests} icon={Handshake} label="Sponsorship Requests" intent="success" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileSettings} icon={Settings} label="Settings" onClick={() => setIsMenuOpen(false)} />
</div>
{/* Demo-specific info */}
{isDemo && (
<Box px={4} py={2} borderTop borderColor="border-charcoal-outline/50" mt={1}>
<Text size="xs" color="text-gray-500" italic>Demo users have limited profile access</Text>
</Box>
)}
</Box>
{/* Footer */}
<Box borderTop borderColor="border-charcoal-outline">
{isDemo ? (
<Box
as="button"
type="button"
onClick={handleLogout}
display="flex"
alignItems="center"
justifyContent="between"
fullWidth
px={4}
py={3}
cursor="pointer"
transition
bg="transparent"
hoverBg="rgba(239, 68, 68, 0.05)"
hoverColor="text-racing-red"
>
<Text size="sm" color="text-gray-500">Logout</Text>
<Icon icon={LogOut} size={4} color="rgb(107, 114, 128)" />
</Box>
) : (
<Box as="form" action="/auth/logout" method="POST">
<Box
as="button"
type="submit"
display="flex"
alignItems="center"
justifyContent="between"
fullWidth
px={4}
py={3}
cursor="pointer"
transition
bg="transparent"
hoverBg="rgba(239, 68, 68, 0.1)"
hoverColor="text-red-400"
>
<Text size="sm" color="text-gray-500">Logout</Text>
<Icon icon={LogOut} size={4} color="rgb(107, 114, 128)" />
</Box>
</Box>
)}
</Box>
</Box>
</Box>
)}
</AnimatePresence>
</Box>
<UserDropdownFooter>
<UserDropdownItem
icon={LogOut}
label="Logout"
intent="critical"
onClick={isDemo ? handleLogout : undefined}
/>
</UserDropdownFooter>
</UserDropdown>
</div>
);
}