website refactor
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Award, Trophy, Medal, Star, Crown, Target, Zap } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { AchievementDisplay } from '@/lib/display-objects/AchievementDisplay';
|
||||
|
||||
interface Achievement {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
rarity: 'common' | 'rare' | 'epic' | 'legendary' | string;
|
||||
earnedAt: Date;
|
||||
}
|
||||
|
||||
interface AchievementGridProps {
|
||||
achievements: Achievement[];
|
||||
}
|
||||
|
||||
function getAchievementIcon(icon: string) {
|
||||
switch (icon) {
|
||||
case 'trophy': return Trophy;
|
||||
case 'medal': return Medal;
|
||||
case 'star': return Star;
|
||||
case 'crown': return Crown;
|
||||
case 'target': return Target;
|
||||
case 'zap': return Zap;
|
||||
default: return Award;
|
||||
}
|
||||
}
|
||||
|
||||
export function AchievementGrid({ achievements }: AchievementGridProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Heading level={2} icon={<Icon icon={Award} size={5} color="#facc15" />}>
|
||||
Achievements
|
||||
</Heading>
|
||||
<Text size="sm" color="text-gray-500" weight="normal">{achievements.length} earned</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Grid cols={1} gap={4}>
|
||||
{achievements.map((achievement) => {
|
||||
const AchievementIcon = getAchievementIcon(achievement.icon);
|
||||
const rarity = AchievementDisplay.getRarityColor(achievement.rarity);
|
||||
return (
|
||||
<Surface
|
||||
key={achievement.id}
|
||||
variant={rarity.surface}
|
||||
rounded="xl"
|
||||
border
|
||||
padding={4}
|
||||
>
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Surface variant="muted" rounded="lg" padding={3}>
|
||||
<Icon icon={AchievementIcon} size={5} color={rarity.icon} />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Text weight="semibold" size="sm" color="text-white" block>{achievement.title}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>{achievement.description}</Text>
|
||||
<Stack direction="row" align="center" gap={2} mt={2}>
|
||||
<Text size="xs" color={rarity.text} weight="medium">
|
||||
{achievement.rarity.toUpperCase()}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500">•</Text>
|
||||
<Text size="xs" color="text-gray-500">
|
||||
{AchievementDisplay.formatDate(achievement.earnedAt)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
interface CareerStatsProps {
|
||||
stats: {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
consistency: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export function CareerStats({ stats }: CareerStatsProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Heading level={2} icon={<Icon icon={TrendingUp} size={5} color="#10b981" />}>
|
||||
Career Statistics
|
||||
</Heading>
|
||||
</Box>
|
||||
<Grid cols={2} gap={4}>
|
||||
<StatItem label="Races" value={stats.totalRaces} />
|
||||
<StatItem label="Wins" value={stats.wins} color="text-performance-green" />
|
||||
<StatItem label="Podiums" value={stats.podiums} color="text-warning-amber" />
|
||||
<StatItem label="Consistency" value={`${stats.consistency}%`} color="text-primary-blue" />
|
||||
</Grid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function StatItem({ label, value, color = 'text-white' }: { label: string, value: string | number, color?: string }) {
|
||||
return (
|
||||
<Box p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626', textAlign: 'center' }}>
|
||||
<Text size="3xl" weight="bold" color={color as any} block mb={1}>{value}</Text>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>{label}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Star, Trophy } from 'lucide-react';
|
||||
|
||||
interface DriverRatingProps {
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
export default function DriverRating({ rating, rank }: DriverRatingProps) {
|
||||
return (
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px]">
|
||||
<span className="inline-flex items-center gap-1 text-amber-300">
|
||||
<Star className="h-3 w-3" />
|
||||
<span className="tabular-nums">
|
||||
{rating !== null ? rating : '—'}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{rank !== null && (
|
||||
<span className="inline-flex items-center gap-1 text-primary-blue">
|
||||
<Trophy className="h-3 w-3" />
|
||||
<span className="tabular-nums">#{rank}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import DriverRating from '@/components/profile/DriverRatingPill';
|
||||
import { PlaceholderImage } from '@/ui/PlaceholderImage';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
export interface DriverSummaryPillProps {
|
||||
driver: DriverViewModel;
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
avatarSrc?: string | null;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function DriverSummaryPill(props: DriverSummaryPillProps) {
|
||||
const { driver, rating, rank, avatarSrc, onClick, href } = props;
|
||||
|
||||
const resolvedAvatar = avatarSrc;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Box width={8} height={8} rounded="full" className="overflow-hidden bg-charcoal-outline flex items-center justify-center border border-charcoal-outline/80">
|
||||
{resolvedAvatar ? (
|
||||
<Image
|
||||
src={resolvedAvatar}
|
||||
alt={driver.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={32} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Stack direction="col" align="start" justify="center" className="leading-tight">
|
||||
<Text size="xs" weight="semibold" color="text-white" className="truncate max-w-[140px]" block>
|
||||
{driver.name}
|
||||
</Text>
|
||||
|
||||
<DriverRating rating={rating} rank={rank} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
const baseClasses = "flex items-center gap-3 rounded-full bg-iron-gray/70 px-3 py-1.5 border border-charcoal-outline/80 shadow-[0_0_18px_rgba(0,0,0,0.45)] hover:border-primary-blue/60 hover:bg-iron-gray transition-colors";
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={baseClasses}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={baseClasses}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="flex items-center gap-3 rounded-full bg-iron-gray/70 px-3 py-1.5 border border-charcoal-outline/80">
|
||||
{content}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Users } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
|
||||
import { mediaConfig } from '@/lib/config/mediaConfig';
|
||||
|
||||
interface Friend {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
interface FriendsPreviewProps {
|
||||
friends: Friend[];
|
||||
}
|
||||
|
||||
export function FriendsPreview({ friends }: FriendsPreviewProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Heading level={2} icon={<Icon icon={Users} size={5} color="#a855f7" />}>
|
||||
Friends
|
||||
</Heading>
|
||||
<Text size="sm" color="text-gray-500" weight="normal">({friends.length})</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack direction="row" gap={3} wrap>
|
||||
{friends.slice(0, 8).map((friend) => (
|
||||
<Box key={friend.id}>
|
||||
<Link
|
||||
href={`/drivers/${friend.id}`}
|
||||
variant="ghost"
|
||||
>
|
||||
<Surface variant="muted" rounded="xl" border padding={2} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', backgroundColor: 'rgba(38, 38, 38, 0.5)', borderColor: '#262626' }}>
|
||||
<Box style={{ width: '2rem', height: '2rem', borderRadius: '9999px', overflow: 'hidden', background: 'linear-gradient(to bottom right, #3b82f6, #9333ea)' }}>
|
||||
<Image
|
||||
src={friend.avatarUrl || mediaConfig.avatars.defaultFallback}
|
||||
alt={friend.name}
|
||||
width={32}
|
||||
height={32}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</Box>
|
||||
<Text size="sm" color="text-white">{friend.name}</Text>
|
||||
<Text size="lg">{CountryFlagDisplay.fromCountryCode(friend.country).toString()}</Text>
|
||||
</Surface>
|
||||
</Link>
|
||||
</Box>
|
||||
))}
|
||||
{friends.length > 8 && (
|
||||
<Box p={2}>
|
||||
<Text size="sm" color="text-gray-500">+{friends.length - 8} more</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
interface League {
|
||||
leagueId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
membershipRole?: string;
|
||||
}
|
||||
|
||||
interface LeagueListItemProps {
|
||||
league: League;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export function LeagueListItem({ league, isAdmin }: LeagueListItemProps) {
|
||||
return (
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="lg"
|
||||
border
|
||||
padding={4}
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderColor: '#262626' }}
|
||||
>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text weight="medium" color="text-white" block>{league.name}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1} style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
||||
{league.description}
|
||||
</Text>
|
||||
{league.membershipRole && (
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
Your role:{' '}
|
||||
<Text color="text-gray-400" style={{ textTransform: 'capitalize' }}>{league.membershipRole}</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Stack direction="row" align="center" gap={2} style={{ marginLeft: '1rem' }}>
|
||||
<Link
|
||||
href={`/leagues/${league.leagueId}`}
|
||||
variant="ghost"
|
||||
>
|
||||
<Text size="sm" color="text-gray-300">View</Text>
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link href={`/leagues/${league.leagueId}?tab=admin`} variant="ghost">
|
||||
<Button variant="primary" size="sm">
|
||||
Manage
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Car, Download, Trash2, Edit } from 'lucide-react';
|
||||
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';
|
||||
|
||||
interface DriverLiveryItem {
|
||||
id: string;
|
||||
carId: string;
|
||||
carName: string;
|
||||
thumbnailUrl: string;
|
||||
uploadedAt: Date;
|
||||
isValidated: boolean;
|
||||
}
|
||||
|
||||
interface LiveryCardProps {
|
||||
livery: DriverLiveryItem;
|
||||
onEdit?: (id: string) => void;
|
||||
onDownload?: (id: string) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
export function LiveryCard({ livery, onEdit, onDownload, onDelete }: LiveryCardProps) {
|
||||
return (
|
||||
<Card className="overflow-hidden hover:border-primary-blue/50 transition-colors">
|
||||
{/* Livery Preview */}
|
||||
<Box height={48} backgroundColor="deep-graphite" rounded="lg" mb={4} display="flex" center border borderColor="charcoal-outline">
|
||||
<Icon icon={Car} size={16} color="text-gray-600" />
|
||||
</Box>
|
||||
|
||||
{/* Livery Info */}
|
||||
<Stack gap={3}>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Heading level={3}>{livery.carName}</Heading>
|
||||
{livery.isValidated ? (
|
||||
<Badge variant="success">
|
||||
Validated
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="warning">
|
||||
Pending
|
||||
</Badge>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Text size="xs" color="text-gray-500">
|
||||
Uploaded {new Date(livery.uploadedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
|
||||
{/* Actions */}
|
||||
<Stack direction="row" gap={2} pt={2}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
fullWidth
|
||||
onClick={() => onEdit?.(livery.id)}
|
||||
icon={<Icon icon={Edit} size={4} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onDownload?.(livery.id)}
|
||||
icon={<Icon icon={Download} size={4} />}
|
||||
aria-label="Download"
|
||||
>
|
||||
{null}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => onDelete?.(livery.id)}
|
||||
icon={<Icon icon={Trash2} size={4} />}
|
||||
aria-label="Delete"
|
||||
>
|
||||
{null}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Activity, TrendingUp, Target, BarChart3 } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { GridItem } from '@/ui/GridItem';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { CircularProgress } from '@/components/charts/CircularProgress';
|
||||
import { HorizontalBarChart } from '@/components/charts/HorizontalBarChart';
|
||||
|
||||
interface PerformanceOverviewProps {
|
||||
stats: {
|
||||
wins: number;
|
||||
podiums: number;
|
||||
totalRaces: number;
|
||||
consistency: number | null;
|
||||
dnfs: number;
|
||||
bestFinish: number;
|
||||
avgFinish: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Box mb={6}>
|
||||
<Heading level={2} icon={<Icon icon={Activity} size={5} color="#00f2ff" />}>
|
||||
Performance Overview
|
||||
</Heading>
|
||||
</Box>
|
||||
<Grid cols={12} gap={8}>
|
||||
<GridItem colSpan={12} lgSpan={6}>
|
||||
<Stack align="center" gap={4}>
|
||||
<Stack direction="row" gap={6}>
|
||||
<CircularProgress
|
||||
value={stats.wins}
|
||||
max={stats.totalRaces}
|
||||
label="Win Rate"
|
||||
color="#10b981"
|
||||
/>
|
||||
<CircularProgress
|
||||
value={stats.podiums}
|
||||
max={stats.totalRaces}
|
||||
label="Podium Rate"
|
||||
color="#f59e0b"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" gap={6}>
|
||||
<CircularProgress
|
||||
value={stats.consistency ?? 0}
|
||||
max={100}
|
||||
label="Consistency"
|
||||
color="#3b82f6"
|
||||
/>
|
||||
<CircularProgress
|
||||
value={stats.totalRaces - stats.dnfs}
|
||||
max={stats.totalRaces}
|
||||
label="Finish Rate"
|
||||
color="#00f2ff"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={12} lgSpan={6}>
|
||||
<Box mb={4}>
|
||||
<Heading level={3} icon={<Icon icon={BarChart3} size={4} color="#9ca3af" />}>
|
||||
Results Breakdown
|
||||
</Heading>
|
||||
</Box>
|
||||
<HorizontalBarChart
|
||||
data={[
|
||||
{ label: 'Wins', value: stats.wins, color: 'bg-performance-green' },
|
||||
{ label: 'Podiums (2nd-3rd)', value: stats.podiums - stats.wins, color: 'bg-warning-amber' },
|
||||
{ label: 'DNFs', value: stats.dnfs, color: 'bg-red-500' },
|
||||
]}
|
||||
maxValue={stats.totalRaces}
|
||||
/>
|
||||
|
||||
<Box mt={6}>
|
||||
<Grid cols={2} gap={4}>
|
||||
<Box p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
|
||||
<Stack gap={2}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={TrendingUp} size={4} color="#10b981" />
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Best Finish</Text>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-performance-green">P{stats.bestFinish}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
|
||||
<Stack gap={2}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Target} size={4} color="#3b82f6" />
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Avg Finish</Text>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-primary-blue">
|
||||
P{(stats.avgFinish ?? 0).toFixed(1)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Box>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { User } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
interface ProfileBioProps {
|
||||
bio: string;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import Button from '../ui/Button';
|
||||
import DriverRatingPill from '@/components/profile/DriverRatingPill';
|
||||
import CountryFlag from '@/ui/CountryFlag';
|
||||
import PlaceholderImage from '@/ui/PlaceholderImage';
|
||||
|
||||
interface ProfileHeaderProps {
|
||||
driver: DriverViewModel;
|
||||
rating?: number | null;
|
||||
rank?: number | null;
|
||||
isOwnProfile?: boolean;
|
||||
onEditClick?: () => void;
|
||||
teamName?: string | null;
|
||||
teamTag?: string | null;
|
||||
}
|
||||
|
||||
export default function ProfileHeader({
|
||||
driver,
|
||||
rating,
|
||||
rank,
|
||||
isOwnProfile = false,
|
||||
onEditClick,
|
||||
teamName,
|
||||
teamTag,
|
||||
}: ProfileHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary-blue to-purple-600 overflow-hidden flex items-center justify-center">
|
||||
{driver.avatarUrl ? (
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={80} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-3xl font-bold text-white">{driver.name}</h1>
|
||||
{driver.country && <CountryFlag countryCode={driver.country} size="lg" />}
|
||||
{teamTag && (
|
||||
<span className="px-3 py-1 bg-primary-blue/20 text-primary-blue rounded-full text-sm font-medium">
|
||||
{teamTag}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span>iRacing ID: {driver.iracingId}</span>
|
||||
{teamName && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-primary-blue">
|
||||
{teamTag ? `[${teamTag}] ${teamName}` : teamName}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(typeof rating === 'number' || typeof rank === 'number') && (
|
||||
<div className="mt-2">
|
||||
<DriverRatingPill rating={rating ?? null} rank={rank ?? null} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOwnProfile && (
|
||||
<Button variant="secondary" onClick={onEditClick}>
|
||||
Edit Profile
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Star, Trophy, Globe, Calendar, Clock, UserPlus, ExternalLink, LucideIcon } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { mediaConfig } from '@/lib/config/mediaConfig';
|
||||
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
|
||||
|
||||
interface ProfileHeroProps {
|
||||
driver: {
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
country: string;
|
||||
iracingId: number;
|
||||
joinedAt: string | Date;
|
||||
};
|
||||
stats: {
|
||||
rating: number;
|
||||
} | null;
|
||||
globalRank: number;
|
||||
timezone: string;
|
||||
socialHandles: {
|
||||
platform: string;
|
||||
handle: string;
|
||||
url: string;
|
||||
}[];
|
||||
onAddFriend: () => void;
|
||||
friendRequestSent: boolean;
|
||||
}
|
||||
|
||||
function getSocialIcon(platform: string) {
|
||||
const { Twitter, Youtube, Twitch, MessageCircle } = require('lucide-react');
|
||||
switch (platform) {
|
||||
case 'twitter': return Twitter;
|
||||
case 'youtube': return Youtube;
|
||||
case 'twitch': return Twitch;
|
||||
case 'discord': return MessageCircle;
|
||||
default: return Globe;
|
||||
}
|
||||
}
|
||||
|
||||
export function ProfileHero({
|
||||
driver,
|
||||
stats,
|
||||
globalRank,
|
||||
timezone,
|
||||
socialHandles,
|
||||
onAddFriend,
|
||||
friendRequestSent,
|
||||
}: ProfileHeroProps) {
|
||||
return (
|
||||
<Surface variant="muted" rounded="2xl" border padding={6} style={{ background: 'linear-gradient(to bottom right, rgba(38, 38, 38, 0.8), rgba(38, 38, 38, 0.6), #0f1115)', borderColor: '#262626' }}>
|
||||
<Stack direction="row" align="start" gap={6} wrap>
|
||||
{/* Avatar */}
|
||||
<Box style={{ position: 'relative' }}>
|
||||
<Box style={{ width: '7rem', height: '7rem', borderRadius: '1rem', background: 'linear-gradient(to bottom right, #3b82f6, #9333ea)', padding: '0.25rem', boxShadow: '0 20px 25px -5px rgba(59, 130, 246, 0.2)' }}>
|
||||
<Box style={{ width: '100%', height: '100%', borderRadius: '0.75rem', overflow: 'hidden', backgroundColor: '#262626' }}>
|
||||
<Image
|
||||
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
|
||||
alt={driver.name}
|
||||
width={144}
|
||||
height={144}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Driver Info */}
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack direction="row" align="center" gap={3} wrap mb={2}>
|
||||
<Heading level={1}>{driver.name}</Heading>
|
||||
<Text size="4xl" aria-label={`Country: ${driver.country}`}>
|
||||
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Rating and Rank */}
|
||||
<Stack direction="row" align="center" gap={4} wrap mb={4}>
|
||||
{stats && (
|
||||
<>
|
||||
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
|
||||
<Text font="mono" weight="bold" color="text-primary-blue">{stats.rating}</Text>
|
||||
<Text size="xs" color="text-gray-400">Rating</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(250, 204, 21, 0.1)', border: '1px solid rgba(250, 204, 21, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} />
|
||||
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>#{globalRank}</Text>
|
||||
<Text size="xs" color="text-gray-400">Global</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Meta info */}
|
||||
<Stack direction="row" align="center" gap={4} wrap style={{ fontSize: '0.875rem', color: '#9ca3af' }}>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Globe style={{ width: '1rem', height: '1rem' }} />
|
||||
<Text size="sm">iRacing: {driver.iracingId}</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Calendar style={{ width: '1rem', height: '1rem' }} />
|
||||
<Text size="sm">
|
||||
Joined{' '}
|
||||
{new Date(driver.joinedAt).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Clock style={{ width: '1rem', height: '1rem' }} />
|
||||
<Text size="sm">{timezone}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAddFriend}
|
||||
disabled={friendRequestSent}
|
||||
icon={<UserPlus style={{ width: '1rem', height: '1rem' }} />}
|
||||
>
|
||||
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{/* Social Handles */}
|
||||
{socialHandles.length > 0 && (
|
||||
<Box mt={6} pt={6} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.5)' }}>
|
||||
<Stack direction="row" align="center" gap={2} wrap>
|
||||
<Text size="sm" color="text-gray-500" style={{ marginRight: '0.5rem' }}>Connect:</Text>
|
||||
{socialHandles.map((social) => {
|
||||
const Icon = getSocialIcon(social.platform);
|
||||
return (
|
||||
<Box key={social.platform}>
|
||||
<Link
|
||||
href={social.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="ghost"
|
||||
>
|
||||
<Surface variant="muted" rounded="lg" padding={1} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', paddingLeft: '0.75rem', paddingRight: '0.75rem', backgroundColor: 'rgba(38, 38, 38, 0.5)', border: '1px solid #262626', color: '#9ca3af' }}>
|
||||
<Icon style={{ width: '1rem', height: '1rem' }} />
|
||||
<Text size="sm">{social.handle}</Text>
|
||||
<ExternalLink style={{ width: '0.75rem', height: '0.75rem', opacity: 0.5 }} />
|
||||
</Surface>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface ProfileLayoutShellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function ProfileLayoutShell({ children }: ProfileLayoutShellProps) {
|
||||
return <div className="min-h-screen bg-deep-graphite">{children}</div>;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface Stat {
|
||||
label: string;
|
||||
value: string | number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface ProfileStatGridProps {
|
||||
stats: Stat[];
|
||||
}
|
||||
|
||||
export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
|
||||
return (
|
||||
<Grid cols={2} gap={4}>
|
||||
{stats.map((stat, idx) => (
|
||||
<Box key={idx} p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626', textAlign: 'center' }}>
|
||||
<Text size="3xl" weight="bold" color={stat.color as any} block mb={1}>{stat.value}</Text>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>{stat.label}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { User, BarChart3, TrendingUp } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
export type ProfileTab = 'overview' | 'stats' | 'ratings';
|
||||
|
||||
interface ProfileTabsProps {
|
||||
activeTab: ProfileTab;
|
||||
onTabChange: (tab: ProfileTab) => void;
|
||||
}
|
||||
|
||||
export function ProfileTabs({ activeTab, onTabChange }: ProfileTabsProps) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Flag, Users, UserPlus } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
interface RacingProfileProps {
|
||||
racingStyle: string;
|
||||
favoriteTrack: string;
|
||||
favoriteCar: string;
|
||||
availableHours: string;
|
||||
lookingForTeam: boolean;
|
||||
openToRequests: boolean;
|
||||
}
|
||||
|
||||
export function RacingProfile({
|
||||
racingStyle,
|
||||
favoriteTrack,
|
||||
favoriteCar,
|
||||
availableHours,
|
||||
lookingForTeam,
|
||||
openToRequests,
|
||||
}: RacingProfileProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Heading level={2} icon={<Icon icon={Flag} size={5} color="#00f2ff" />}>
|
||||
Racing Profile
|
||||
</Heading>
|
||||
</Box>
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Racing Style</Text>
|
||||
<Text color="text-white" weight="medium">{racingStyle}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Favorite Track</Text>
|
||||
<Text color="text-white" weight="medium">{favoriteTrack}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Favorite Car</Text>
|
||||
<Text color="text-white" weight="medium">{favoriteCar}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block>Available</Text>
|
||||
<Text color="text-white" weight="medium">{availableHours}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Status badges */}
|
||||
<Box mt={4} pt={4} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.5)' }}>
|
||||
<Stack gap={2}>
|
||||
{lookingForTeam && (
|
||||
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', border: '1px solid rgba(16, 185, 129, 0.3)' }}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Users} size={4} color="#10b981" />
|
||||
<Text size="sm" color="text-performance-green" weight="medium">Looking for Team</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
)}
|
||||
{openToRequests && (
|
||||
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)' }}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={UserPlus} size={4} color="#3b82f6" />
|
||||
<Text size="sm" color="text-primary-blue" weight="medium">Open to Friend Requests</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Shield, Users, ChevronRight } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
interface TeamMembership {
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
role: string;
|
||||
joinedAt: Date;
|
||||
}
|
||||
|
||||
interface TeamMembershipGridProps {
|
||||
memberships: TeamMembership[];
|
||||
}
|
||||
|
||||
export function TeamMembershipGrid({ memberships }: TeamMembershipGridProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Box mb={4}>
|
||||
<Heading level={2} icon={<Shield style={{ width: '1.25rem', height: '1.25rem', color: '#a855f7' }} />}>
|
||||
Team Memberships
|
||||
<Text size="sm" color="text-gray-500" weight="normal" style={{ marginLeft: '0.5rem' }}>({memberships.length})</Text>
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: '1rem' }}>
|
||||
{memberships.map((membership) => (
|
||||
<Box key={membership.team.id}>
|
||||
<Link
|
||||
href={`/teams/${membership.team.id}`}
|
||||
variant="ghost"
|
||||
>
|
||||
<Surface variant="muted" rounded="xl" border padding={4} style={{ display: 'flex', alignItems: 'center', gap: '1rem', backgroundColor: 'rgba(38, 38, 38, 0.3)', borderColor: '#262626' }}>
|
||||
<Surface variant="muted" rounded="lg" padding={3} style={{ backgroundColor: 'rgba(147, 51, 234, 0.2)', border: '1px solid rgba(147, 51, 234, 0.3)' }}>
|
||||
<Users style={{ width: '1.5rem', height: '1.5rem', color: '#a855f7' }} />
|
||||
</Surface>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text weight="semibold" color="text-white" block truncate>{membership.team.name}</Text>
|
||||
<Stack direction="row" align="center" gap={2} mt={1}>
|
||||
<Surface variant="muted" rounded="full" padding={1} style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem', backgroundColor: 'rgba(147, 51, 234, 0.2)', color: '#a855f7' }}>
|
||||
<Text size="xs" weight="medium" style={{ textTransform: 'capitalize' }}>{membership.role}</Text>
|
||||
</Surface>
|
||||
<Text size="xs" color="text-gray-500">Since {membership.joinedAt.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373' }} />
|
||||
</Surface>
|
||||
</Link>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
|
||||
import { BarChart3, Building2, ChevronDown, CreditCard, Handshake, LogOut, Megaphone, Paintbrush, Settings, TrendingUp, Trophy, Shield } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { CapabilityGate } from '@/components/shared/CapabilityGate';
|
||||
import { useEffectiveDriverId } from '@/lib/hooks/useEffectiveDriverId';
|
||||
import type { DriverViewModel } from '@/lib/view-models/view-models/DriverViewModel';
|
||||
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/view-models/DriverViewModel';
|
||||
import { useFindDriverById } from '@/lib/hooks/driver/useFindDriverById';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
BarChart3,
|
||||
ChevronDown,
|
||||
CreditCard,
|
||||
Handshake,
|
||||
LogOut,
|
||||
Megaphone,
|
||||
Paintbrush,
|
||||
Settings,
|
||||
Shield
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/DriverViewModel';
|
||||
import { useFindDriverById } from '@/hooks/driver/useFindDriverById';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Image } from '@/ui/Image';
|
||||
|
||||
// Hook to detect demo user mode based on session
|
||||
function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } {
|
||||
@@ -80,65 +92,10 @@ function useHasAdminAccess(): boolean {
|
||||
displayName.includes('super admin');
|
||||
}
|
||||
|
||||
// Sponsor Pill Component - matches the style of DriverSummaryPill
|
||||
function SponsorSummaryPill({
|
||||
onClick,
|
||||
companyName = 'Acme Racing Co.',
|
||||
activeSponsors = 7,
|
||||
impressions = 127,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
companyName?: string;
|
||||
activeSponsors?: number;
|
||||
impressions?: number;
|
||||
}) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
onClick={onClick}
|
||||
className="group flex items-center gap-3 rounded-full bg-gradient-to-r from-iron-gray to-deep-graphite border border-charcoal-outline px-3 py-1.5 hover:border-performance-green/50 transition-all duration-200"
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.02 }}
|
||||
whileTap={shouldReduceMotion ? {} : { scale: 0.98 }}
|
||||
>
|
||||
{/* Avatar/Logo */}
|
||||
<div className="relative">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-performance-green/20 to-performance-green/5 border border-performance-green/30 flex items-center justify-center">
|
||||
<Building2 className="w-4 h-4 text-performance-green" />
|
||||
</div>
|
||||
{/* Active indicator */}
|
||||
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-performance-green border-2 border-deep-graphite" />
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="hidden sm:flex flex-col items-start">
|
||||
<span className="text-xs font-semibold text-white truncate max-w-[100px]">
|
||||
{companyName.split(' ')[0]}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-gray-500">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Trophy className="w-2.5 h-2.5 text-performance-green" />
|
||||
{activeSponsors}
|
||||
</span>
|
||||
<span className="text-gray-600">•</span>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<TrendingUp className="w-2.5 h-2.5 text-primary-blue" />
|
||||
{impressions}k
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chevron */}
|
||||
<ChevronDown className="w-3.5 h-3.5 text-gray-500 group-hover:text-gray-300 transition-colors" />
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserPill() {
|
||||
export function UserPill() {
|
||||
const { session } = useAuth();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const { isDemo, demoRole } = useDemoUserMode();
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const primaryDriverId = useEffectiveDriverId();
|
||||
|
||||
@@ -153,43 +110,6 @@ export default function UserPill() {
|
||||
return new DriverViewModelClass({ ...driverDto, avatarUrl: driverDto.avatarUrl ?? null });
|
||||
}, [driverDto]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!session?.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Demo users don't have real driver data
|
||||
if (isDemo) {
|
||||
return {
|
||||
isDemo: true,
|
||||
demoRole,
|
||||
displayName: session.user.displayName,
|
||||
email: session.user.email,
|
||||
avatarUrl: session.user.avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (!primaryDriverId || !driver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Driver rating + rank are not exposed by the current API contract for the lightweight
|
||||
// driver DTO used in the header. Keep it null until the API provides it.
|
||||
const rating: number | null = null;
|
||||
const rank: number | null = null;
|
||||
|
||||
const avatarSrc = driver.avatarUrl;
|
||||
|
||||
return {
|
||||
driver,
|
||||
avatarSrc,
|
||||
rating,
|
||||
rank,
|
||||
isDemo: false,
|
||||
demoRole: null,
|
||||
};
|
||||
}, [session, driver, primaryDriverId, isDemo, demoRole]);
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
@@ -223,20 +143,32 @@ export default function UserPill() {
|
||||
// Handle unauthenticated users
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Link
|
||||
href=routes.auth.login
|
||||
className="inline-flex items-center gap-2 rounded-full bg-iron-gray border border-charcoal-outline px-4 py-1.5 text-xs font-medium text-gray-300 hover:text-white hover:border-gray-500 transition-all"
|
||||
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
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary-blue px-4 py-1.5 text-xs font-semibold text-white shadow-[0_0_12px_rgba(25,140,255,0.5)] hover:bg-primary-blue/90 hover:shadow-[0_0_18px_rgba(25,140,255,0.8)] transition-all"
|
||||
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>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,7 +176,7 @@ export default function UserPill() {
|
||||
// Determine what to show in the pill
|
||||
const displayName = driver?.name || session.user.displayName || session.user.email || 'User';
|
||||
const avatarUrl = session.user.avatarUrl;
|
||||
const roleLabel = isDemo ? {
|
||||
const roleLabel = isDemo ? ({
|
||||
'driver': 'Driver',
|
||||
'sponsor': 'Sponsor',
|
||||
'league-owner': 'League Owner',
|
||||
@@ -252,232 +184,294 @@ export default function UserPill() {
|
||||
'league-admin': 'League Admin',
|
||||
'system-owner': 'System Owner',
|
||||
'super-admin': 'Super Admin',
|
||||
}[demoRole || 'driver'] : null;
|
||||
} as Record<string, string>)[demoRole || 'driver'] : null;
|
||||
|
||||
const roleColor = isDemo ? {
|
||||
const roleColor = isDemo ? ({
|
||||
'driver': 'text-primary-blue',
|
||||
'sponsor': 'text-performance-green',
|
||||
'league-owner': 'text-purple-400',
|
||||
'league-steward': 'text-amber-400',
|
||||
'league-steward': 'text-warning-amber',
|
||||
'league-admin': 'text-red-400',
|
||||
'system-owner': 'text-indigo-400',
|
||||
'super-admin': 'text-pink-400',
|
||||
}[demoRole || 'driver'] : null;
|
||||
} as Record<string, string>)[demoRole || 'driver'] : null;
|
||||
|
||||
return (
|
||||
<div className="relative inline-flex items-center" data-user-pill>
|
||||
<motion.button
|
||||
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => setIsMenuOpen((open) => !open)}
|
||||
className="group flex items-center gap-3 rounded-full bg-gradient-to-r from-iron-gray to-deep-graphite border border-charcoal-outline px-3 py-1.5 hover:border-primary-blue/50 transition-all duration-200"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
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)'}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className="relative">
|
||||
<Box position="relative">
|
||||
{avatarUrl ? (
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-charcoal-outline flex items-center justify-center border border-charcoal-outline/80">
|
||||
<img
|
||||
<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">
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={displayName}
|
||||
className="w-full h-full object-cover"
|
||||
objectFit="cover"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30 flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-primary-blue">
|
||||
<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">
|
||||
{displayName[0]?.toUpperCase() || 'U'}
|
||||
</span>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-primary-blue border-2 border-deep-graphite" />
|
||||
</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>
|
||||
|
||||
{/* Info */}
|
||||
<div className="hidden sm:flex flex-col items-start">
|
||||
<span className="text-xs font-semibold text-white truncate max-w-[100px]">
|
||||
<Box display={{ base: 'none', sm: 'flex' }} flexDirection="col" alignItems="start">
|
||||
<Text size="xs" weight="semibold" color="text-white" truncate maxWidth="100px" block>
|
||||
{displayName}
|
||||
</span>
|
||||
</Text>
|
||||
{roleLabel && (
|
||||
<span className={`text-[10px] ${roleColor} font-medium`}>
|
||||
<Text size="xs" color={roleColor || 'text-gray-400'} weight="medium" fontSize="10px">
|
||||
{roleLabel}
|
||||
</span>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* Chevron */}
|
||||
<ChevronDown className="w-3.5 h-3.5 text-gray-500 group-hover:text-gray-300 transition-colors" />
|
||||
</motion.button>
|
||||
<Icon icon={ChevronDown} size={3.5} color="rgb(107, 114, 128)" groupHoverTextColor="text-gray-300" />
|
||||
</Box>
|
||||
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
className="absolute right-0 top-full mt-2 w-56 rounded-xl bg-deep-graphite border border-charcoal-outline shadow-xl shadow-black/30 z-50 overflow-hidden"
|
||||
<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}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={`p-4 bg-gradient-to-r ${isDemo ? 'from-primary-blue/10' : 'from-iron-gray/20'} to-transparent border-b border-charcoal-outline`}>
|
||||
<div className="flex items-center gap-3">
|
||||
{avatarUrl ? (
|
||||
<div className="w-10 h-10 rounded-lg overflow-hidden bg-charcoal-outline flex items-center justify-center border border-charcoal-outline/80">
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={displayName}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30 flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-primary-blue">
|
||||
{displayName[0]?.toUpperCase() || 'U'}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{displayName}</p>
|
||||
{roleLabel && (
|
||||
<p className={`text-xs ${roleColor}`}>{roleLabel}</p>
|
||||
)}
|
||||
{isDemo && (
|
||||
<p className="text-xs text-gray-500">Demo Account</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isDemo && (
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Development account - not for production use
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="py-1 text-sm text-gray-200">
|
||||
{/* Admin link for Owner/Super Admin users */}
|
||||
{hasAdminAccess && (
|
||||
{/* 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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Regular user profile links */}
|
||||
<Link
|
||||
href="/admin"
|
||||
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
href={routes.protected.profile}
|
||||
block
|
||||
px={4}
|
||||
py={2.5}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Shield className="h-4 w-4 text-indigo-400" />
|
||||
<span>Admin Area</span>
|
||||
<Text size="sm" color="text-gray-200">Profile</Text>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Sponsor portal link for demo sponsor users */}
|
||||
{isDemo && demoRole === 'sponsor' && (
|
||||
<>
|
||||
<Link
|
||||
href="/sponsor"
|
||||
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4 text-performance-green" />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
<Link
|
||||
href=routes.sponsor.campaigns
|
||||
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Megaphone className="h-4 w-4 text-primary-blue" />
|
||||
<span>My Sponsorships</span>
|
||||
</Link>
|
||||
<Link
|
||||
href=routes.sponsor.billing
|
||||
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<CreditCard className="h-4 w-4 text-warning-amber" />
|
||||
<span>Billing</span>
|
||||
</Link>
|
||||
<Link
|
||||
href=routes.sponsor.settings
|
||||
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Settings className="h-4 w-4 text-gray-400" />
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Regular user profile links */}
|
||||
<Link
|
||||
href=routes.protected.profile
|
||||
className="block px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
<Link
|
||||
href=routes.protected.profileLeagues
|
||||
className="block px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Manage leagues
|
||||
</Link>
|
||||
<Link
|
||||
href=routes.protected.profileLiveries
|
||||
className="flex items-center gap-2 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Paintbrush className="h-4 w-4" />
|
||||
<span>Liveries</span>
|
||||
</Link>
|
||||
<Link
|
||||
href=routes.protected.profileSponsorshipRequests
|
||||
className="flex items-center gap-2 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Handshake className="h-4 w-4 text-performance-green" />
|
||||
<span>Sponsorship Requests</span>
|
||||
</Link>
|
||||
<Link
|
||||
href=routes.protected.profileSettings
|
||||
className="flex items-center gap-2 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
|
||||
{/* Demo-specific info */}
|
||||
{isDemo && (
|
||||
<div className="px-4 py-2 text-xs text-gray-500 italic border-t border-charcoal-outline/50 mt-1">
|
||||
Demo users have limited profile access
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-charcoal-outline">
|
||||
{isDemo ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center justify-between px-4 py-3 text-sm text-gray-500 hover:text-racing-red hover:bg-racing-red/5 transition-colors"
|
||||
<Link
|
||||
href={routes.protected.profileLeagues}
|
||||
block
|
||||
px={4}
|
||||
py={2.5}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<span>Logout</span>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<form action="/auth/logout" method="POST">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full items-center justify-between px-4 py-3 text-sm text-gray-500 hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
<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>
|
||||
|
||||
{/* 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"
|
||||
>
|
||||
<span>Logout</span>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
<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>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user