website refactor

This commit is contained in:
2026-01-19 01:24:07 +01:00
parent e1ce3bffd1
commit edc4cd7f21
64 changed files with 1113 additions and 753 deletions

View File

@@ -3,6 +3,7 @@
import { LeagueListItem } from '@/components/leagues/LeagueListItem';
import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { ProfileSection } from './ProfileSection';
import React from 'react';
@@ -22,7 +23,7 @@ interface MembershipPanelProps {
export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanelProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<Stack gap={8}>
<ProfileSection
title="Leagues You Own"
description="Manage the leagues you have created and lead."
@@ -34,11 +35,11 @@ export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanel
</Text>
</Card>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Stack gap={3}>
{ownedLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} isAdmin />
))}
</div>
</Stack>
)}
</ProfileSection>
@@ -53,13 +54,13 @@ export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanel
</Text>
</Card>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Stack gap={3}>
{memberLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} />
))}
</div>
</Stack>
)}
</ProfileSection>
</div>
</Stack>
);
}

View File

@@ -3,6 +3,8 @@
import { Panel } from '@/ui/Panel';
import { Select } from '@/ui/Select';
import { Toggle } from '@/ui/Toggle';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { ProfileStatsGroup, ProfileStat } from '@/ui/ProfileHero';
import React from 'react';
@@ -22,7 +24,7 @@ export function PreferencesPanel({ preferences, isEditing, onUpdate }: Preferenc
if (isEditing) {
return (
<Panel title="Racing Preferences">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Stack gap={6}>
<Select
label="Favorite Car Class"
value={preferences.favoriteCarClass}
@@ -45,19 +47,21 @@ export function PreferencesPanel({ preferences, isEditing, onUpdate }: Preferenc
]}
/>
<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>
<Box paddingTop={2}>
<Stack gap={3}>
<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>
</Box>
</Stack>
</Panel>
);
}

View File

@@ -5,6 +5,9 @@ import { Panel } from '@/ui/Panel';
import { Input } from '@/ui/Input';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { ProfileStat } from '@/ui/ProfileHero';
import React from 'react';
@@ -22,7 +25,7 @@ export function ProfileDetailsPanel({ driver, isEditing, onUpdate }: ProfileDeta
if (isEditing) {
return (
<Panel title="Profile Details">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Stack gap={6}>
<Input
label="Nationality (ISO Code)"
value={driver.country}
@@ -35,31 +38,31 @@ export function ProfileDetailsPanel({ driver, isEditing, onUpdate }: ProfileDeta
onChange={(e) => onUpdate?.({ bio: e.target.value })}
placeholder="Tell the community about your racing career..."
/>
</div>
</Stack>
</Panel>
);
}
return (
<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' }}>
<Stack gap={6}>
<Stack gap={1}>
<Text size="xs" variant="low" weight="bold" uppercase block>Nationality</Text>
<Group gap={2}>
<Text size="xl">
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
<Text variant="med">{driver.country}</Text>
</div>
</div>
</Group>
</Stack>
<div>
<Text size="xs" variant="low" weight="bold" uppercase block marginBottom={1}>Bio</Text>
<Stack gap={1}>
<Text size="xs" variant="low" weight="bold" uppercase block>Bio</Text>
<Text variant="med" leading="relaxed">
{driver.bio || 'No bio provided.'}
</Text>
</div>
</div>
</Stack>
</Stack>
</Panel>
);
}

View File

@@ -7,6 +7,9 @@ import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '@/ui/ProfileHero';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Calendar, Globe, Star, Trophy, UserPlus } from 'lucide-react';
import React from 'react';
@@ -37,7 +40,7 @@ export function ProfileHeader({
}: ProfileHeaderProps) {
return (
<ProfileHero variant="muted">
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem', flexWrap: 'wrap' }}>
<Box display="flex" alignItems="center" gap={8} flexWrap="wrap">
<ProfileAvatar>
<Image
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
@@ -48,27 +51,29 @@ export function ProfileHeader({
/>
</ProfileAvatar>
<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>
<Stack flex={1} minWidth="200px" gap={1}>
<Box marginBottom={1}>
<Group gap={3}>
<Heading level={1}>{driver.name}</Heading>
<Text size="2xl" aria-label={`Country: ${driver.country}`}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
</Group>
</Box>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<Group gap={4}>
<Group gap={1.5}>
<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' }}>
</Group>
<Group gap={1.5}>
<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>
</div>
</div>
</div>
</Group>
</Group>
</Stack>
<ProfileStatsGroup>
{stats && (
@@ -80,7 +85,7 @@ export function ProfileHeader({
</ProfileStatsGroup>
{!isOwnProfile && onAddFriend && (
<div style={{ marginLeft: 'auto' }}>
<Box marginLeft="auto">
<Button
variant={friendRequestSent ? 'secondary' : 'primary'}
onClick={onAddFriend}
@@ -90,9 +95,9 @@ export function ProfileHeader({
>
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
</div>
</Box>
)}
</div>
</Box>
</ProfileHero>
);
}

View File

@@ -1,6 +1,7 @@
'use client';
import { SectionHeader } from '@/ui/SectionHeader';
import { Box } from '@/ui/Box';
import React from 'react';
interface ProfileSectionProps {
@@ -12,14 +13,14 @@ interface ProfileSectionProps {
export function ProfileSection({ title, description, action, children }: ProfileSectionProps) {
return (
<section style={{ marginBottom: '2rem' }}>
<Box as="section" marginBottom={8}>
<SectionHeader
title={title}
description={description}
actions={action}
variant="minimal"
/>
<div>{children}</div>
</section>
<Box>{children}</Box>
</Box>
);
}

View File

@@ -9,6 +9,10 @@ import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { UserDropdown, UserDropdownHeader, UserDropdownItem, UserDropdownFooter } from '@/ui/UserDropdown';
import { AnimatePresence, motion } from 'framer-motion';
import {
@@ -143,7 +147,7 @@ export function UserPill() {
// Handle unauthenticated users
if (!session) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Group gap={2}>
<Link
href={routes.auth.login}
variant="secondary"
@@ -156,7 +160,7 @@ export function UserPill() {
>
Get Started
</Link>
</div>
</Group>
);
}
@@ -185,7 +189,7 @@ export function UserPill() {
} as Record<string, 'primary' | 'success' | 'warning' | 'critical'>)[demoRole || 'driver'] : 'low';
return (
<div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }} data-user-pill>
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
<button
type="button"
onClick={() => setIsMenuOpen((open) => !open)}
@@ -201,26 +205,26 @@ export function UserPill() {
}}
>
{/* Avatar */}
<div style={{ position: 'relative' }}>
<Box position="relative">
{avatarUrl ? (
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Surface width="2rem" height="2rem" rounded="full" overflow="hidden" border>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
/>
</div>
</Surface>
) : (
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Surface width="2rem" height="2rem" rounded="full" variant="gradient-blue" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</div>
</Surface>
)}
</div>
</Box>
{/* Info */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'start' }}>
<Stack align="start" gap={0}>
<Text size="xs" weight="semibold" variant="high" truncate style={{ maxWidth: '100px' }}>
{displayName}
</Text>
@@ -229,7 +233,7 @@ export function UserPill() {
{roleLabel}
</Text>
)}
</div>
</Stack>
{/* Chevron */}
<Icon icon={ChevronDown} size={3.5} intent="low" />
@@ -237,32 +241,32 @@ export function UserPill() {
<UserDropdown isOpen={isMenuOpen}>
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<Group gap={3}>
{avatarUrl ? (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Surface width="2.5rem" height="2.5rem" rounded="md" overflow="hidden" border>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
/>
</div>
</Surface>
) : (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Surface width="2.5rem" height="2.5rem" rounded="md" variant="gradient-blue" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</div>
</Surface>
)}
<div>
<Stack gap={0}>
<Text size="sm" weight="semibold" variant="high" block>{displayName}</Text>
{roleLabel && (
<Text size="xs" variant="low" block>{roleLabel}</Text>
)}
</div>
</div>
</Stack>
</Group>
</UserDropdownHeader>
<div style={{ padding: '0.25rem 0' }}>
<Box paddingY={1}>
{hasAdminAccess && (
<UserDropdownItem href="/admin" icon={Shield} label="Admin Area" intent="primary" onClick={() => setIsMenuOpen(false)} />
)}
@@ -280,7 +284,7 @@ export function UserPill() {
<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>
</Box>
<UserDropdownFooter>
<UserDropdownItem
@@ -291,6 +295,6 @@ export function UserPill() {
/>
</UserDropdownFooter>
</UserDropdown>
</div>
</Box>
);
}