website refactor

This commit is contained in:
2026-01-18 18:27:39 +01:00
parent 067502a4c6
commit c35682cae5
36 changed files with 47 additions and 108 deletions

View File

@@ -0,0 +1,17 @@
import React from 'react';
interface AppFooterProps {
children?: React.ReactNode;
className?: string;
}
/**
* AppFooter is the bottom section of the application.
*/
export function AppFooter({ children, className = '' }: AppFooterProps) {
return (
<footer className={`bg-[#141619] border-t border-[#23272B] py-8 px-4 md:px-6 ${className}`}>
{children}
</footer>
);
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
interface AppHeaderProps {
children: React.ReactNode;
className?: string;
}
/**
* AppHeader is the top control bar of the application.
* It follows the "Telemetry Workspace" structure.
*/
export function AppHeader({ children, className = '' }: AppHeaderProps) {
return (
<header
className={`sticky top-0 z-50 h-16 md:h-20 bg-[#0C0D0F]/80 backdrop-blur-md border-b border-[#23272B] flex items-center px-4 md:px-6 ${className}`}
>
{children}
</header>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
interface AppShellProps {
children: React.ReactNode;
className?: string;
}
/**
* AppShell is the root container for the entire application layout.
* It provides the base background and layout structure.
*/
export function AppShell({ children, className = '' }: AppShellProps) {
return (
<div className={`min-h-screen bg-[#0C0D0F] text-gray-100 flex flex-col ${className}`}>
{children}
</div>
);
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
interface AppSidebarProps {
children?: React.ReactNode;
className?: string;
}
/**
* AppSidebar is the "dashboard rail" of the application.
* It provides global navigation and context.
*/
export function AppSidebar({ children, className = '' }: AppSidebarProps) {
return (
<aside
className={`hidden lg:flex flex-col w-64 bg-[#141619] border-r border-[#23272B] overflow-y-auto ${className}`}
>
{children}
</aside>
);
}

View File

@@ -0,0 +1,17 @@
import { ErrorBanner } from '../../ui/ErrorBanner';
interface AuthErrorProps {
action: string;
}
export function AuthError({ action }: AuthErrorProps) {
return (
<ErrorBanner
message={`Failed to load ${action} page`}
title="Error"
variant="error"
/>
);
}

View File

@@ -0,0 +1,27 @@
import { LoadingSpinner } from '../../ui/LoadingSpinner';
import { Box } from '../../ui/primitives/Box';
import { Stack } from '../../ui/primitives/Stack';
import { Text } from '../../ui/Text';
interface AuthLoadingProps {
message?: string;
}
export function AuthLoading({ message = 'Authenticating...' }: AuthLoadingProps) {
return (
<Box
fullWidth
minHeight="100vh"
display="flex"
center
bg="bg-[#0f1115]"
>
<Stack align="center" gap={4}>
<LoadingSpinner size={10} />
<Text color="text-gray-400" weight="medium">
{message}
</Text>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,146 @@
import { Flag, Star, Trophy, Users } from 'lucide-react';
import { Avatar } from '../../ui/Avatar';
import { Badge } from '../../ui/Badge';
import { Heading } from '../../ui/Heading';
import { Icon } from '../../ui/Icon';
import { Box } from '../../ui/primitives/Box';
import { Stack } from '../../ui/primitives/Stack';
import { Text } from '../../ui/Text';
interface DashboardHeroProps {
driverName: string;
avatarUrl?: string | null;
rating: number;
rank: number;
totalRaces: number;
winRate: number;
className?: string;
}
export function DashboardHero({
driverName,
avatarUrl,
rating,
rank,
totalRaces,
winRate,
className = '',
}: DashboardHeroProps) {
return (
<Box
position="relative"
bg="bg-[#0C0D0F]"
borderBottom
borderColor="border-[#23272B]"
overflow="hidden"
className={className}
>
{/* Background Glow */}
<Box
position="absolute"
top={-100}
right={-100}
w="500px"
h="500px"
bg="bg-primary-blue/10"
rounded="full"
blur="3xl"
/>
<Box p={{ base: 6, md: 10 }} position="relative" zIndex={10}>
<Stack direction={{ base: 'col', md: 'row' }} align="center" gap={8}>
{/* Avatar Section */}
<Box position="relative">
<Box
p={1}
rounded="2xl"
bg="bg-[#141619]"
border
borderColor="border-[#23272B]"
>
<Avatar
src={avatarUrl}
alt={driverName}
size={120}
className="rounded-xl"
/>
</Box>
<Box
position="absolute"
bottom={-2}
right={-2}
w="10"
h="10"
rounded="xl"
bg="bg-[#4ED4E0]"
borderWidth="2px"
borderStyle="solid"
borderColor="border-[#0C0D0F]"
display="flex"
center
>
<Icon icon={Star} size={5} color="#0C0D0F" />
</Box>
</Box>
{/* Info Section */}
<Stack flex={1} align={{ base: 'center', md: 'start' }} gap={4}>
<Box>
<Heading level={1} uppercase letterSpacing="tighter" mb={2}>
{driverName}
</Heading>
<Stack direction="row" gap={4}>
<Stack gap={0.5}>
<Text size="xs" color="text-gray-500" uppercase>Rating</Text>
<Text size="sm" weight="bold" color="text-[#4ED4E0]" font="mono">{rating}</Text>
</Stack>
<Stack gap={0.5}>
<Text size="xs" color="text-gray-500" uppercase>Rank</Text>
<Text size="sm" weight="bold" color="text-[#FFBE4D]" font="mono">#{rank}</Text>
</Stack>
<Stack gap={0.5}>
<Text size="xs" color="text-gray-500" uppercase>Starts</Text>
<Text size="sm" weight="bold" color="text-gray-300" font="mono">{totalRaces}</Text>
</Stack>
</Stack>
</Box>
<Stack direction="row" gap={3} wrap>
<Badge variant="primary" rounded="lg" icon={Trophy}>
{winRate}% Win Rate
</Badge>
<Badge variant="info" rounded="lg" icon={Flag}>
Pro License
</Badge>
<Badge variant="default" rounded="lg" icon={Users}>
Team Redline
</Badge>
</Stack>
</Stack>
{/* Quick Stats */}
<Stack
direction="row"
gap={4}
p={6}
bg="bg-white/5"
rounded="2xl"
border
borderColor="border-white/10"
className="backdrop-blur-md"
>
<Stack align="center" px={4}>
<Text size="2xl" weight="bold" color="text-white">12</Text>
<Text size="xs" color="text-gray-500" uppercase>Podiums</Text>
</Stack>
<Box w="1px" h="10" bg="bg-white/10" />
<Stack align="center" px={4}>
<Text size="2xl" weight="bold" color="text-white">4</Text>
<Text size="xs" color="text-gray-500" uppercase>Wins</Text>
</Stack>
</Stack>
</Stack>
</Box>
</Box>
);
}

View File

@@ -1,6 +1,6 @@
import { DashboardHero as UiDashboardHero } from '@/components/dashboard/DashboardHero';
import { routes } from '@/lib/routing/RouteConfig';
import { Button } from '@/ui/Button';
import { DashboardHero as UiDashboardHero } from '@/ui/DashboardHero';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { StatBox } from '@/ui/StatBox';

View File

@@ -0,0 +1,59 @@
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/primitives/Stack';
interface OnboardingCTAProps {
onBack?: () => void;
onNext?: () => void;
nextLabel?: string;
backLabel?: string;
isLastStep?: boolean;
canNext?: boolean;
isLoading?: boolean;
type?: 'button' | 'submit';
}
export function OnboardingCTA({
onBack,
onNext,
nextLabel = 'Continue',
backLabel = 'Back',
isLastStep = false,
canNext = true,
isLoading = false,
type = 'button',
}: OnboardingCTAProps) {
return (
<Stack direction="row" justify="between" mt={8} gap={4}>
{onBack ? (
<Button
type="button"
variant="secondary"
onClick={onBack}
disabled={isLoading}
className="px-8"
>
{backLabel}
</Button>
) : (
<div />
)}
<Button
type={type}
variant="primary"
onClick={onNext}
disabled={isLoading || !canNext}
className="px-8 min-w-[140px]"
>
{isLoading ? (
<Stack direction="row" gap={2} align="center">
<span className="animate-spin"></span>
<span>Processing...</span>
</Stack>
) : (
isLastStep ? 'Complete Setup' : nextLabel
)}
</Button>
</Stack>
);
}

View File

@@ -0,0 +1,5 @@
export function OnboardingCardAccent() {
return (
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-bl from-primary-blue/10 to-transparent rounded-bl-full" />
);
}

View File

@@ -0,0 +1,11 @@
interface OnboardingContainerProps {
children: React.ReactNode;
}
export function OnboardingContainer({ children }: OnboardingContainerProps) {
return (
<div className="max-w-3xl mx-auto px-4 py-10">
{children}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { Box } from '../../ui/primitives/Box';
import { Text } from '../../ui/Text';
interface OnboardingErrorProps {
message: string;
}
export function OnboardingError({ message }: OnboardingErrorProps) {
return (
<Box
mt={6}
display="flex"
alignItems="start"
gap={3}
p={4}
rounded="xl"
bg="bg-red-500/10"
border
borderColor="border-red-500/30"
>
<Text color="text-red-400" flexShrink={0} mt={0.5}></Text>
<Text size="sm" color="text-red-400">{message}</Text>
</Box>
);
}

View File

@@ -0,0 +1,15 @@
import { FormEvent, ReactNode } from 'react';
import { Box } from '../../ui/primitives/Box';
interface OnboardingFormProps {
children: ReactNode;
onSubmit: (e: FormEvent) => void;
}
export function OnboardingForm({ children, onSubmit }: OnboardingFormProps) {
return (
<Box as="form" onSubmit={onSubmit} position="relative">
{children}
</Box>
);
}

View File

@@ -0,0 +1,17 @@
interface OnboardingHeaderProps {
title: string;
subtitle: string;
emoji: string;
}
export function OnboardingHeader({ title, subtitle, emoji }: OnboardingHeaderProps) {
return (
<div className="text-center mb-8">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-blue/20 to-purple-600/10 border border-primary-blue/30 mx-auto mb-4">
<span className="text-2xl">{emoji}</span>
</div>
<h1 className="text-4xl font-bold mb-2">{title}</h1>
<p className="text-gray-400">{subtitle}</p>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export function OnboardingHelpText() {
return (
<p className="text-center text-xs text-gray-500 mt-6">
Your avatar will be AI-generated based on your photo and chosen suit color
</p>
);
}

View File

@@ -0,0 +1,58 @@
import { Button } from '@/ui/Button';
interface OnboardingNavigationProps {
onBack: () => void;
onNext?: () => void;
isLastStep: boolean;
canSubmit: boolean;
loading: boolean;
}
export function OnboardingNavigation({ onBack, onNext, isLastStep, canSubmit, loading }: OnboardingNavigationProps) {
return (
<div className="mt-8 flex items-center justify-between">
<Button
type="button"
variant="secondary"
onClick={onBack}
disabled={loading}
className="flex items-center gap-2"
>
<span></span>
Back
</Button>
{!isLastStep ? (
<Button
type="button"
variant="primary"
onClick={onNext}
disabled={loading}
className="flex items-center gap-2"
>
Continue
<span></span>
</Button>
) : (
<Button
type="submit"
variant="primary"
disabled={loading || !canSubmit}
className="flex items-center gap-2"
>
{loading ? (
<>
<span className="animate-spin"></span>
Creating Profile...
</>
) : (
<>
<span></span>
Complete Setup
</>
)}
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
interface OnboardingStepHeaderProps {
title: string;
description?: string;
}
export function OnboardingStepHeader({ title, description }: OnboardingStepHeaderProps) {
return (
<Stack gap={1} mb={6}>
<Text size="2xl" color="text-white" weight="bold" className="tracking-tight" block>
{title}
</Text>
{description && (
<Text size="sm" color="text-gray-400" block>
{description}
</Text>
)}
</Stack>
);
}

View File

@@ -0,0 +1,25 @@
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/primitives/Box';
import { Text } from '@/ui/Text';
import { User } from 'lucide-react';
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>
);
}

View File

@@ -0,0 +1,34 @@
import { Box } from '../../ui/primitives/Box';
import { Grid } from '../../ui/primitives/Grid';
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} mdCols={4} gap={4}>
{stats.map((stat, idx) => (
<Box
key={idx}
p={4}
bg="bg-[#0f1115]"
rounded="xl"
border
borderColor="border-[#262626]"
textAlign="center"
>
<Text size="3xl" weight="bold" color={stat.color} block mb={1}>{stat.value}</Text>
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em">{stat.label}</Text>
</Box>
))}
</Grid>
);
}

View File

@@ -0,0 +1,47 @@
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/primitives/Box';
import { Surface } from '@/ui/primitives/Surface';
import { BarChart3, TrendingUp, User } from 'lucide-react';
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>
);
}