website refactor

This commit is contained in:
2026-01-19 12:35:16 +01:00
parent a8731e6937
commit 15290400b3
122 changed files with 902 additions and 255 deletions

View File

@@ -1,11 +1,13 @@
import { Flag, Star, Trophy, Users } from 'lucide-react';
import { Avatar } from '../../ui/Avatar';
import { Badge } from '../../ui/Badge';
import { Box } from '../../ui/Box';
import { Heading } from '../../ui/Heading';
import { Icon } from '../../ui/Icon';
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '../../ui/ProfileHero';
import { BadgeGroup } from '../../ui/BadgeGroup';
import { QuickStatCard, QuickStatItem } from '../../ui/QuickStatCard';
import { Stack } from '../../ui/Stack';
import React from 'react';
interface DashboardHeroProps {
@@ -27,7 +29,7 @@ export function DashboardHero({
}: DashboardHeroProps) {
return (
<ProfileHero glowColor="aqua">
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem', flexWrap: 'wrap' }}>
<Stack direction="row" align="center" gap={8} wrap>
{/* Avatar Section */}
<ProfileAvatar
badge={<Icon icon={Star} size={5} intent="high" />}
@@ -40,7 +42,7 @@ export function DashboardHero({
</ProfileAvatar>
{/* Info Section */}
<div style={{ flex: 1, minWidth: '200px' }}>
<Box flex={1} minWidth="200px">
<Heading level={1}>{driverName}</Heading>
<ProfileStatsGroup>
@@ -60,14 +62,14 @@ export function DashboardHero({
Team Redline
</Badge>
</BadgeGroup>
</div>
</Box>
{/* Quick Stats */}
<QuickStatCard>
<QuickStatItem label="Podiums" value="12" />
<QuickStatItem label="Wins" value="4" />
</QuickStatCard>
</div>
</Stack>
</ProfileHero>
);
}

View File

@@ -5,6 +5,7 @@ import { routes } from '@/lib/routing/RouteConfig';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { StatGrid } from '@/ui/StatGrid';
import { Flag, Medal, Target, Trophy, User, Users } from 'lucide-react';
import React from 'react';
@@ -26,7 +27,7 @@ interface DashboardHeroProps {
export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHeroProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<Stack gap={8}>
<UiDashboardHero
driverName={currentDriver.name}
avatarUrl={currentDriver.avatarUrl}
@@ -36,7 +37,7 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
winRate={Math.round((currentDriver.wins / currentDriver.totalRaces) * 100) || 0}
/>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<Stack direction="row" gap={4} wrap>
<Link href={routes.public.leagues}>
<Button variant="secondary" icon={<Icon icon={Flag} size={4} />}>
Browse Leagues
@@ -47,7 +48,7 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
View Profile
</Button>
</Link>
</div>
</Stack>
<StatGrid
variant="box"
@@ -59,6 +60,6 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
{ icon: Users, label: 'Active Leagues', value: activeLeaguesCount, intent: 'telemetry' },
]}
/>
</div>
</Stack>
);
}

View File

@@ -4,7 +4,7 @@ import { useNotifications } from '@/components/notifications/NotificationProvide
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/CircuitBreakerRegistry';
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench, X } from 'lucide-react';
import { useEffect, useState } from 'react';

View File

@@ -2,11 +2,12 @@
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { IconButton } from '@/ui/IconButton';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Box, Clock, Copy, Download, Play, Trash2 } from 'lucide-react';
import { Clock, Copy, Download, Play, Trash2 } from 'lucide-react';
import { useEffect, useState } from 'react';
interface ReplayEntry {

View File

@@ -11,8 +11,7 @@ import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { ControlBar } from '@/ui/ControlBar';
import { Trophy } from 'lucide-react';
import { useEffect, useState } from 'react';
import React from 'react';
import React, { useEffect, useState } from 'react';
interface RaceHistoryProps {
driverId: string;

View File

@@ -3,10 +3,12 @@ import { Badge } from '@/ui/Badge';
interface RatingBadgeProps {
rating: number;
size?: 'sm' | 'md';
size?: 'sm' | 'md' | 'lg';
}
export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
const badgeSize = size === 'lg' ? 'md' : size;
const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => {
if (val >= 2500) return 'warning';
if (val >= 2000) return 'primary'; // Simplified
@@ -18,7 +20,7 @@ export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
return (
<Badge
variant={getVariant(rating)}
size={size}
size={badgeSize}
>
{rating.toLocaleString()}
</Badge>

View File

@@ -56,7 +56,7 @@ export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProp
border
bg={getBgColor(rating)}
borderColor={getBorderColor(rating)}
{...sizeProps[size]}
{...(sizeProps[size] as any)}
>
<Shield size={iconSizes[size]} color={iconColors[colorClass as keyof typeof iconColors]} />
<Text

View File

@@ -3,7 +3,7 @@
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { DevErrorPanel } from './DevErrorPanel';
import { DevErrorPanel } from '@/ui/DevErrorPanel';
import { ErrorDisplay } from '@/ui/ErrorDisplay';
interface Props {

View File

@@ -1,3 +1,4 @@
import { Stack } from '@/ui/Stack';
import React, { ReactNode } from 'react';
interface ActivityFeedListProps {
@@ -6,8 +7,8 @@ interface ActivityFeedListProps {
export function ActivityFeedList({ children }: ActivityFeedListProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
{children}
</div>
</Stack>
);
}

View File

@@ -1,5 +1,6 @@
import { FeedItemCard } from '@/components/feed/FeedItemCard';
import { FeedEmptyState } from '@/ui/FeedEmptyState';
import { Stack } from '@/ui/Stack';
import React from 'react';
interface FeedItemData {
@@ -23,10 +24,10 @@ export function FeedList({ items }: FeedListProps) {
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
{items.map(item => (
<FeedItemCard key={item.id} item={item} />
))}
</div>
</Stack>
);
}

View File

@@ -1,7 +1,7 @@
import { routes } from '@/lib/routing/RouteConfig';
import { Stack } from '@/ui/Stack';
import { Calendar, Home, Layout, Settings, Trophy, Users } from 'lucide-react';
import { NavLink } from './NavLink';
import { NavLink } from '@/ui/NavLink';
interface AuthedNavProps {
pathname: string;

View File

@@ -1,7 +1,7 @@
import { routes } from '@/lib/routing/RouteConfig';
import { Stack } from '@/ui/Stack';
import { Calendar, Home, Layout, Trophy, Users } from 'lucide-react';
import { NavLink } from './NavLink';
import { NavLink } from '@/ui/NavLink';
interface PublicNavProps {
pathname: string;

View File

@@ -5,10 +5,12 @@ import React from 'react';
interface RankBadgeProps {
rank: number;
size?: 'sm' | 'md';
size?: 'sm' | 'md' | 'lg';
}
export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
const badgeSize = size === 'lg' ? 'md' : size;
const getVariant = (rank: number): 'warning' | 'primary' | 'info' | 'default' => {
if (rank <= 3) return 'warning';
if (rank <= 10) return 'primary';
@@ -28,7 +30,7 @@ export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
const medal = getMedalEmoji(rank);
return (
<Badge variant={getVariant(rank)} size={size}>
<Badge variant={getVariant(rank)} size={badgeSize}>
<Group gap={1}>
{medal && <Text size="xs">{medal}</Text>}
<Text size="xs" weight="bold">#{rank}</Text>

View File

@@ -1,6 +1,7 @@
import { DriverIdentity } from '@/ui/DriverIdentity';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { TableCell, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
@@ -41,7 +42,7 @@ export function LeagueMemberRow({
return (
<TableRow variant={isTopPerformer ? 'highlight' : 'default'}>
<TableCell>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Box display="flex" alignItems="center" gap={2}>
{driver ? (
<DriverIdentity
driver={driver}
@@ -59,7 +60,7 @@ export function LeagueMemberRow({
{isTopPerformer && (
<Text size="xs"></Text>
)}
</div>
</Box>
</TableCell>
<TableCell>
<Text variant="primary" weight="medium">

View File

@@ -17,8 +17,7 @@ import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { ControlBar } from '@/ui/ControlBar';
import { useCallback, useEffect, useState } from 'react';
import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
interface LeagueMembersProps {
leagueId: string;

View File

@@ -7,7 +7,7 @@ import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueSchedu
import { useState } from 'react';
// Shared state components
import { StateContainer } from '@/ui/StateContainer';
import { StateContainer } from '@/components/shared/state/StateContainer';
import { useLeagueSchedule } from "@/hooks/league/useLeagueSchedule";
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';

View File

@@ -1,7 +1,6 @@
'use client';
import { Text } from '@/ui/Text';
import { useState } from 'react';
import { MediaCard } from '@/ui/MediaCard';
import { MediaFiltersBar } from './MediaFiltersBar';
import { Grid } from '@/ui/Grid';
@@ -10,7 +9,7 @@ import { MediaViewerModal } from './MediaViewerModal';
import { SectionHeader } from '@/ui/SectionHeader';
import { EmptyState } from '@/ui/EmptyState';
import { Search } from 'lucide-react';
import React from 'react';
import React, { useState } from 'react';
export interface MediaAsset {
id: string;

View File

@@ -235,7 +235,7 @@ function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boo
return (
<Stack as={motion.span} weight="bold" color="text-primary-accent" font="mono" size={{ base: 'sm', sm: 'base' }}>
{shouldReduceMotion ? value : <Stack as={motion.span}>{rounded}</Stack>}
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}
</Stack>
);
}
@@ -265,7 +265,7 @@ function AnimatedCounter({
return (
<Stack weight="bold" color="text-white" font="mono" size={{ base: 'sm', sm: 'base', md: 'lg' }}>
{shouldReduceMotion ? value : <Stack as={motion.span}>{rounded}</Stack>}{suffix}
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}{suffix}
</Stack>
);
}

View File

@@ -270,7 +270,7 @@ function AnimatedPoints({
weight="bold"
color="text-white"
>
{shouldReduceMotion ? points : <Stack as={motion.span}>{spring}</Stack>}
{shouldReduceMotion ? points : <motion.span>{spring}</motion.span>}
</Stack>
</Stack>
</Stack>

View File

@@ -6,6 +6,7 @@ interface Stat {
label: string;
value: string | number;
intent?: 'primary' | 'telemetry' | 'success' | 'critical';
color?: string;
}
interface ProfileStatGridProps {
@@ -17,6 +18,7 @@ export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
label: stat.label,
value: stat.value,
intent: stat.intent || 'primary',
color: stat.color,
icon: Bug // Default icon if none provided, but StatBox requires one
}));

View File

@@ -190,7 +190,8 @@ export function UserPill() {
return (
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
<button
<Box
as="button"
type="button"
onClick={() => setIsMenuOpen((open) => !open)}
style={{
@@ -237,7 +238,7 @@ export function UserPill() {
{/* Chevron */}
<Icon icon={ChevronDown} size={3.5} intent="low" />
</button>
</Box>
<UserDropdown isOpen={isMenuOpen}>
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>

View File

@@ -35,7 +35,7 @@ export function LatestResultsSidebar({ results }: LatestResultsSidebarProps) {
<RaceSummaryItem
track={result.track}
meta={`${result.winnerName}${result.car}`}
date={scheduledAt}
dateLabel={scheduledAt.toLocaleDateString()}
/>
</Box>
);

View File

@@ -24,22 +24,22 @@ interface RaceListItemProps {
export function RaceListItem({ race, onClick }: RaceListItemProps) {
const statusConfig = {
scheduled: {
icon: Clock,
iconName: 'Clock',
variant: 'primary' as const,
label: 'Scheduled',
},
running: {
icon: PlayCircle,
iconName: 'PlayCircle',
variant: 'success' as const,
label: 'LIVE',
},
completed: {
icon: CheckCircle2,
iconName: 'CheckCircle2',
variant: 'default' as const,
label: 'Completed',
},
cancelled: {
icon: XCircle,
iconName: 'XCircle',
variant: 'warning' as const,
label: 'Cancelled',
},
@@ -64,11 +64,13 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) {
dayLabel={date.getDate().toString()}
timeLabel={formatTime(race.scheduledAt)}
status={race.status}
statusLabel={config.label}
statusVariant={config.variant}
statusIconName={config.iconName}
leagueName={race.leagueName}
leagueHref={race.leagueId ? routes.league.detail(race.leagueId) : undefined}
strengthOfField={race.strengthOfField}
onClick={() => onClick(race.id)}
statusConfig={config}
/>
);
}

View File

@@ -1,5 +1,6 @@
import { Box } from '@/ui/Box';
import { Panel } from '@/ui/Panel';
import { Stack } from '@/ui/Stack';
import { StatusDot } from '@/ui/StatusDot';
import { Text } from '@/ui/Text';
@@ -30,16 +31,16 @@ export function SessionSummaryPanel({
return (
<Panel title="Session Summary" className={className}>
<Box display="flex" flexDirection="col" gap={3}>
<Box display="flex" align="center" justify="between">
<Stack gap={3}>
<Stack direction="row" align="center" justify="between">
<Text weight="bold" size="lg">{title}</Text>
<Box display="flex" align="center" gap={2}>
<Stack direction="row" align="center" gap={2}>
<StatusDot color={statusColor} pulse={status === 'live'} size={2} />
<Text size="xs" uppercase weight="bold" style={{ color: statusColor }}>
{status}
</Text>
</Box>
</Box>
</Stack>
</Stack>
<Box display="grid" gridCols={2} gap={4} borderTop borderStyle="solid" borderColor="border-gray/10" pt={3}>
{startTime && (
@@ -61,7 +62,7 @@ export function SessionSummaryPanel({
</Box>
)}
</Box>
</Box>
</Stack>
</Panel>
);
}

View File

@@ -1,4 +1,5 @@
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface TelemetryItem {
@@ -25,7 +26,7 @@ export function TelemetryStrip({ items, className = '' }: TelemetryStripProps) {
className={`bg-graphite-black/80 border-y border-border-gray/30 py-2 px-4 flex items-center gap-8 overflow-x-auto no-scrollbar ${className}`}
>
{items.map((item, index) => (
<Box key={index} display="flex" align="center" gap={2} className="whitespace-nowrap">
<Stack key={index} direction="row" align="center" gap={2} className="whitespace-nowrap">
<Text size="xs" color="text-gray-500" uppercase weight="bold" letterSpacing="widest">
{item.label}
</Text>
@@ -42,7 +43,7 @@ export function TelemetryStrip({ items, className = '' }: TelemetryStripProps) {
{item.trend === 'up' ? '↑' : item.trend === 'down' ? '↓' : '•'}
</Text>
)}
</Box>
</Stack>
))}
</Box>
);

View File

@@ -3,6 +3,7 @@
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Grid } from '@/ui/Grid';
import { Droplets, Sun, Thermometer, Wind, type LucideIcon } from 'lucide-react';
interface TrackConditionsPanelProps {
@@ -26,7 +27,7 @@ export function TrackConditionsPanel({
Track Conditions
</Text>
<Stack display="grid" gridCols={2} mdCols={4} gap={4}>
<Grid cols={2} mdCols={4} gap={4}>
<ConditionItem
icon={Thermometer}
label="Air Temp"
@@ -51,7 +52,7 @@ export function TrackConditionsPanel({
value={windSpeed}
color="text-gray-400"
/>
</Stack>
</Grid>
<Stack mt={4} pt={4} borderTop borderColor="border-outline-steel" bgOpacity={0.5} display="flex" alignItems="center" gap={3}>
<Icon icon={Sun} size={4} color="#FFBE4D" />

View File

@@ -42,7 +42,7 @@ export function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) {
key={race.id}
track={race.track}
meta={race.car}
date={scheduledAt}
dateLabel={scheduledAt.toLocaleDateString()}
/>
);
})}

View File

@@ -130,7 +130,8 @@ export function RangeField({
<Group justify="between" gap={3}>
<Text as="label" size="xs" weight="medium" variant="low">{label}</Text>
<Box display="flex" alignItems="center" gap={2} flex={1} maxWidth="200px">
<div
<Box
as="div"
ref={sliderRef}
style={{
position: 'relative',
@@ -173,7 +174,7 @@ export function RangeField({
boxShadow: isDragging ? '0 0 12px rgba(25, 140, 255, 0.5)' : undefined
}}
/>
</div>
</Box>
<Box flexShrink={0}>
<Group gap={1}>
<Text size="sm" weight="semibold" variant="high" textAlign="right" width="2rem">{clampedValue}</Text>
@@ -202,7 +203,8 @@ export function RangeField({
)}
{/* Custom slider */}
<div
<Box
as="div"
ref={sliderRef}
style={{
position: 'relative',
@@ -265,12 +267,13 @@ export function RangeField({
boxShadow: isDragging ? '0 0 16px rgba(25, 140, 255, 0.6)' : '0 2px 8px rgba(0,0,0,0.3)'
}}
/>
</div>
</Box>
{/* Value input and quick presets */}
<Group justify="between" align="center" gap={3}>
<Group gap={2}>
<input
<Box
as="input"
ref={inputRef}
type="number"
min={min}
@@ -301,7 +304,8 @@ export function RangeField({
{quickPresets.length > 0 && (
<Group gap={1}>
{quickPresets.slice(0, 3).map((preset) => (
<button
<Box
as="button"
key={preset}
type="button"
onClick={() => {
@@ -321,7 +325,7 @@ export function RangeField({
}}
>
{preset}
</button>
</Box>
))}
</Group>
)}

View File

@@ -18,6 +18,7 @@ import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { Badge } from '@/ui/Badge';
import { ProgressLine } from '@/ui/ProgressLine';
import { SharedEmptyState } from './SharedEmptyState';
export {
Pagination as SharedPagination,
@@ -38,5 +39,6 @@ export {
Skeleton as SharedSkeleton,
LoadingSpinner as SharedLoadingSpinner,
Badge as SharedBadge,
ProgressLine as SharedProgressLine
ProgressLine as SharedProgressLine,
SharedEmptyState
};

View File

@@ -84,8 +84,7 @@ export function TransactionTable({ transactions, onDownload }: TransactionTableP
return (
<Grid
key={tx.id}
cols={1}
mdCols={12}
cols={{ base: 1, md: 12 }}
gap={4}
p={4}
alignItems="center"

View File

@@ -1,6 +1,7 @@
'use client';
import { useCreateTeam } from "@/hooks/team/useCreateTeam";
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { InfoBanner } from '@/ui/InfoBanner';
import { Input } from '@/ui/Input';
@@ -79,7 +80,7 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
};
return (
<form onSubmit={handleSubmit}>
<Box as="form" onSubmit={handleSubmit}>
<Group direction="col" align="stretch" gap={6}>
<Input
label="Team Name *"
@@ -142,6 +143,6 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
)}
</Group>
</Group>
</form>
</Box>
);
}

View File

@@ -19,8 +19,7 @@ import { SectionHeader } from '@/ui/SectionHeader';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { useState } from 'react';
import React from 'react';
import React, { useState } from 'react';
interface TeamAdminProps {
team: {