website refactor

This commit is contained in:
2026-01-17 02:32:34 +01:00
parent 6a49448e0a
commit 4d5ce9bfd6
43 changed files with 1642 additions and 2022 deletions

View File

@@ -16,21 +16,21 @@ interface BadgeProps {
}
export function Badge({ children, className = '', variant = 'default', size = 'sm', icon, style, bg, color, borderColor }: BadgeProps) {
const baseClasses = 'flex items-center gap-1.5 rounded-full border font-medium';
const baseClasses = 'flex items-center gap-1.5 rounded-none border font-bold uppercase tracking-widest';
const sizeClasses = {
xs: 'px-1.5 py-0.5 text-[10px]',
sm: 'px-2.5 py-1 text-xs',
md: 'px-3 py-1.5 text-sm'
xs: 'px-1.5 py-0.5 text-[9px]',
sm: 'px-2 py-0.5 text-[10px]',
md: 'px-3 py-1 text-xs'
};
const variantClasses = {
default: 'bg-gray-500/10 border-gray-500/30 text-gray-400',
primary: 'bg-primary-blue/10 border-primary-blue/30 text-primary-blue',
success: 'bg-performance-green/10 border-performance-green/30 text-performance-green',
primary: 'bg-primary-accent/10 border-primary-accent/30 text-primary-accent',
success: 'bg-success-green/10 border-success-green/30 text-success-green',
warning: 'bg-warning-amber/10 border-warning-amber/30 text-warning-amber',
danger: 'bg-red-600/10 border-red-600/30 text-red-500',
info: 'bg-neon-aqua/10 border-neon-aqua/30 text-neon-aqua'
danger: 'bg-critical-red/10 border-critical-red/30 text-critical-red',
info: 'bg-telemetry-aqua/10 border-telemetry-aqua/30 text-telemetry-aqua'
};
const classes = [

View File

@@ -6,7 +6,7 @@ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as'
children: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement>;
className?: string;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-performance' | 'race-final' | 'discord';
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-final' | 'discord';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
@@ -34,25 +34,24 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
rel,
...props
}, ref) => {
const baseClasses = 'inline-flex items-center rounded-lg transition-all duration-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 hover:scale-[1.02] active:scale-95';
const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-smooth focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
const variantClasses = {
primary: 'bg-primary-blue text-white hover:bg-primary-blue/80 focus-visible:outline-primary-blue shadow-[0_0_15px_rgba(25,140,255,0.4)]',
secondary: 'bg-iron-gray text-white border border-charcoal-outline hover:bg-iron-gray/80 focus-visible:outline-primary-blue',
danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600',
ghost: 'bg-transparent text-gray-400 hover:bg-gray-800 focus-visible:outline-gray-400',
'race-performance': 'bg-gradient-to-r from-yellow-400 to-orange-500 text-white shadow-[0_0_15px_rgba(251,191,36,0.4)] hover:from-yellow-500 hover:to-orange-600 focus-visible:outline-yellow-400',
'race-final': 'bg-gradient-to-r from-purple-400 to-pink-500 text-white shadow-[0_0_15px_rgba(168,85,247,0.4)] hover:from-purple-500 hover:to-pink-600 focus-visible:outline-purple-400',
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] shadow-[0_0_20px_rgba(88,101,242,0.3)] hover:shadow-[0_0_30px_rgba(88,101,242,0.6)] focus-visible:outline-[#5865F2]'
primary: 'bg-primary-accent text-white hover:bg-primary-accent/90 focus-visible:outline-primary-accent shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
secondary: 'bg-panel-gray text-white border border-border-gray hover:bg-border-gray/50 focus-visible:outline-primary-accent',
danger: 'bg-critical-red text-white hover:bg-critical-red/90 focus-visible:outline-critical-red',
ghost: 'bg-transparent text-gray-400 hover:text-white hover:bg-white/5 focus-visible:outline-gray-400',
'race-final': 'bg-success-green text-graphite-black hover:bg-success-green/90 focus-visible:outline-success-green',
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
};
const sizeClasses = {
sm: 'min-h-[36px] px-3 py-1.5 text-xs',
md: 'min-h-[44px] px-4 py-2 text-sm',
lg: 'min-h-[52px] px-6 py-3 text-base'
sm: 'min-h-[32px] px-3 py-1 text-xs font-medium',
md: 'min-h-[40px] px-4 py-2 text-sm font-medium',
lg: 'min-h-[48px] px-6 py-3 text-base font-medium'
};
const disabledClasses = disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer';
const disabledClasses = disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
const widthClasses = fullWidth ? 'w-full' : '';
const classes = [

View File

@@ -13,7 +13,7 @@ interface CardProps extends Omit<BoxProps<'div'>, 'children' | 'className' | 'on
children: ReactNode;
className?: string;
onClick?: MouseEventHandler<HTMLDivElement>;
variant?: 'default' | 'highlight';
variant?: 'default' | 'outline' | 'ghost';
p?: Spacing | ResponsiveSpacing;
px?: Spacing | ResponsiveSpacing;
py?: Spacing | ResponsiveSpacing;
@@ -30,17 +30,18 @@ export function Card({
variant = 'default',
...props
}: CardProps) {
const baseClasses = 'rounded-lg shadow-card border duration-200';
const baseClasses = 'rounded-none transition-all duration-150 ease-smooth';
const variantClasses = {
default: 'bg-iron-gray border-charcoal-outline',
highlight: 'bg-gradient-to-r from-blue-900/20 to-blue-700/10 border-blue-500/30'
default: 'bg-panel-gray border border-border-gray shadow-card',
outline: 'bg-transparent border border-border-gray',
ghost: 'bg-transparent border-none'
};
const classes = [
baseClasses,
variantClasses[variant],
onClick ? 'cursor-pointer hover:scale-[1.02]' : '',
onClick ? 'cursor-pointer hover:bg-border-gray/30' : '',
className
].filter(Boolean).join(' ');
@@ -52,7 +53,7 @@ export function Card({
<Box
className={classes}
onClick={onClick}
p={hasPadding ? undefined : 6}
p={hasPadding ? undefined : 4}
{...props}
>
{children}

View File

@@ -15,11 +15,11 @@ export function DecorativeBlur({
opacity = 10
}: DecorativeBlurProps) {
const colorClasses = {
blue: 'bg-primary-blue',
green: 'bg-performance-green',
blue: 'bg-primary-accent',
green: 'bg-success-green',
purple: 'bg-purple-600',
yellow: 'bg-yellow-400',
red: 'bg-racing-red'
yellow: 'bg-warning-amber',
red: 'bg-critical-red'
};
const sizeClasses = {

View File

@@ -2,6 +2,9 @@
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Glow } from '@/ui/Glow';
import { Icon } from '@/ui/Icon';
import { DiscordIcon } from '@/ui/icons/DiscordIcon';
import { Stack } from '@/ui/Stack';
@@ -14,21 +17,25 @@ export function DiscordCTA() {
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#';
return (
<Surface
<Box
as="section"
variant="discord"
padding={4}
bg="graphite-black"
position="relative"
py={{ base: 4, md: 12, lg: 16 }}
py={{ base: 20, md: 32 }}
borderBottom
borderColor="border-gray/50"
overflow="hidden"
>
<Box maxWidth="896px" mx="auto" px={2}>
<Glow color="primary" size="xl" position="center" opacity={0.05} />
<Container size="lg" position="relative" zIndex={10}>
<Surface
variant="discord-inner"
padding={3}
variant="default"
padding={12}
border
rounded="xl"
rounded="none"
position="relative"
shadow="discord"
className="overflow-hidden bg-panel-gray/40"
>
{/* Discord brand accent */}
<Box
@@ -37,120 +44,107 @@ export function DiscordCTA() {
left={0}
right={0}
height="1"
backgroundColor="[#5865F2]"
opacity={0.6}
className="bg-gradient-to-r from-transparent via-[#5865F2]/60 to-transparent"
bg="primary-accent"
/>
<Stack align="center" gap={6} center>
<Stack align="center" gap={12} center>
{/* Header */}
<Stack align="center" gap={4}>
<Stack align="center" gap={6}>
<Box
display="flex"
center
rounded="full"
w={{ base: "10", md: "14", lg: "18" }}
h={{ base: "10", md: "14", lg: "18" }}
backgroundColor="[#5865F2]"
opacity={0.2}
rounded="none"
w={{ base: "16", md: "20" }}
h={{ base: "16", md: "20" }}
bg="primary-accent/10"
border
borderColor="[#5865F2]"
borderColor="primary-accent/30"
className="relative"
>
<DiscordIcon color="text-[#5865F2]" size={32} />
<DiscordIcon color="text-primary-accent" size={40} />
<Box position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="primary-accent" />
<Box position="absolute" bottom="-1px" right="-1px" w="2" h="2" borderBottom borderRight borderColor="primary-accent" />
</Box>
<Stack gap={2}>
<Text as="h2" size="2xl" weight="semibold" color="text-white">
Join us on Discord
</Text>
<Stack gap={4} align="center">
<Heading level={2} weight="bold" color="text-white" fontSize={{ base: '2xl', md: '4xl' }} className="tracking-tight">
Join the Grid on Discord
</Heading>
<Box
mx="auto"
rounded="full"
w={{ base: "16", md: "24", lg: "32" }}
h={{ base: "0.5", md: "1" }}
backgroundColor="[#5865F2]"
className="bg-gradient-to-r from-[#5865F2] to-[#7289DA]"
w="16"
h="1"
bg="primary-accent"
/>
</Stack>
</Stack>
{/* Personal message */}
<Box maxWidth="672px" mx="auto">
<Stack gap={3}>
<Text size="sm" color="text-gray-300" weight="normal" leading="relaxed">
GridPilot is a <Text weight="bold" color="text-white">solo developer project</Text>, and I&apos;m building it because I got tired of the chaos in league racing.
<Box maxWidth="2xl" mx="auto" textAlign="center">
<Stack gap={6}>
<Text size="lg" color="text-gray-300" weight="medium" leading="relaxed">
GridPilot is a <span className="text-white font-bold">solo developer project</span> built for the community.
</Text>
<Text size="sm" color="text-gray-400" weight="normal" leading="relaxed">
This is <Text weight="bold" color="text-gray-300">early days</Text>, and I need your help. Join the Discord to:
<Text size="base" color="text-gray-400" weight="normal" leading="relaxed">
We are in early alpha. Join us to help shape the future of motorsport infrastructure. Your feedback directly influences the roadmap.
</Text>
</Stack>
</Box>
{/* Benefits grid */}
<Box
maxWidth="2xl"
maxWidth="4xl"
mx="auto"
mt={4}
fullWidth
>
<Grid cols={2} gap={3} className="md:grid-cols-2">
<Grid cols={1} mdCols={2} gap={6}>
<BenefitItem
icon={MessageSquare}
title="Share your pain points"
description="Tell me what frustrates you about league racing today"
description="Tell us what frustrates you about league racing today."
/>
<BenefitItem
icon={Lightbulb}
title="Shape the product"
description="Your ideas will directly influence what gets built"
description="Your ideas directly influence our roadmap."
/>
<BenefitItem
icon={Users}
title="Be part of the community"
description="Connect with other league racers who get it"
title="Connect with racers"
description="Join a community of like-minded competitive drivers."
/>
<BenefitItem
icon={Code}
title="Get early access"
description="Test features first and help iron out the rough edges"
title="Early Access"
description="Test new features before they go public."
/>
</Grid>
</Box>
{/* CTA Button */}
<Stack gap={3} pt={4}>
<Stack gap={6} pt={4} align="center">
<Button
as="a"
href={discordUrl}
target="_blank"
rel="noopener noreferrer"
variant="discord"
variant="primary"
size="lg"
icon={<DiscordIcon size={28} />}
className="px-16 py-4"
icon={<DiscordIcon size={24} />}
>
Join us on Discord
Join Discord
</Button>
<Text size="xs" color="text-primary-blue" weight="light">
💡 Get a link to our early alpha view in the Discord
</Text>
{!process.env.NEXT_PUBLIC_DISCORD_URL && (
<Text size="xs" color="text-gray-500">
Note: Configure NEXT_PUBLIC_DISCORD_URL in your environment variables
<Box border borderStyle="dashed" borderColor="primary-accent/50" px={4} py={1}>
<Text size="xs" color="text-primary-accent" weight="bold" font="mono" uppercase letterSpacing="widest">
Early Alpha Access Available
</Text>
)}
</Box>
</Stack>
{/* Footer note */}
<Box maxWidth="xl" mx="auto" pt={4}>
<Text size="xs" color="text-gray-500" weight="light" leading="relaxed" align="center" block>
This is a community effort. Every voice matters. Let&apos;s build something that actually works for league racing.
</Text>
</Box>
</Stack>
</Surface>
</Box>
</Surface>
</Container>
</Box>
);
}
@@ -159,30 +153,29 @@ function BenefitItem({ icon, title, description }: { icon: LucideIcon, title: st
<Surface
variant="muted"
border
padding={3}
rounded="lg"
padding={6}
rounded="none"
display="flex"
gap={3}
className="items-start hover:border-[#5865F2]/30 transition-all"
gap={5}
className="items-start hover:border-primary-accent/30 transition-all bg-panel-gray/20 group"
>
<Box
display="flex"
center
rounded="lg"
rounded="none"
flexShrink={0}
w="6"
h="6"
backgroundColor="[#5865F2]"
opacity={0.2}
w="10"
h="10"
bg="primary-accent/5"
border
borderColor="[#5865F2]"
mt={0.5}
borderColor="border-gray/50"
className="group-hover:border-primary-accent/30 transition-colors"
>
<Icon icon={icon} size={4} color="text-[#5865F2]" />
<Icon icon={icon} size={5} color="text-primary-accent" />
</Box>
<Stack gap={0.5}>
<Text size="xs" weight="medium" color="text-white">{title}</Text>
<Text size="xs" color="text-gray-400" leading="relaxed">{description}</Text>
<Stack gap={2}>
<Text size="base" weight="bold" color="text-white" className="tracking-wide">{title}</Text>
<Text size="sm" color="text-gray-400" leading="relaxed">{description}</Text>
</Stack>
</Surface>
);

View File

@@ -8,41 +8,31 @@ const xUrl = process.env.NEXT_PUBLIC_X_URL || '#';
export function Footer() {
return (
<Box as="footer" position="relative" bg="bg-deep-graphite">
<Box position="absolute" top="0" left="0" right="0" h="px" bg="linear-gradient(to right, transparent, var(--primary-blue), transparent)" />
<Box as="footer" position="relative" bg="graphite-black" borderTop borderColor="border-gray/50">
<Box position="absolute" top="0" left="0" right="0" h="px" bg="linear-gradient(to right, transparent, #198CFF, transparent)" opacity={0.3} />
<Box maxWidth="4xl" mx="auto" px={{ base: 'calc(1.5rem+var(--sal))', lg: 8 }} py={{ base: 2, md: 8, lg: 12 }} pb={{ base: 'calc(0.5rem+var(--sab))', md: 'calc(1.5rem+var(--sab))' }}>
<Box maxWidth="7xl" mx="auto" px={{ base: 6, lg: 8 }} py={{ base: 12, md: 16 }}>
{/* Racing stripe accent */}
<Box
display="flex"
gap={1}
mb={{ base: 2, md: 4, lg: 6 }}
gap={2}
mb={8}
justifyContent="center"
>
<Box w={{ base: "12", md: "20", lg: "28" }} h={{ base: "0.5", md: "0.5", lg: "1" }} bg="bg-white" rounded="full" />
<Box w={{ base: "12", md: "20", lg: "28" }} h={{ base: "0.5", md: "0.5", lg: "1" }} bg="bg-primary-blue" rounded="full" />
<Box w={{ base: "12", md: "20", lg: "28" }} h={{ base: "0.5", md: "0.5", lg: "1" }} bg="bg-white" rounded="full" />
<Box w="12" h="1" bg="white" opacity={0.1} />
<Box w="12" h="1" bg="primary-accent" />
<Box w="12" h="1" bg="white" opacity={0.1} />
</Box>
{/* Personal message */}
<Box
textAlign="center"
mb={{ base: 3, md: 6, lg: 8 }}
mb={12}
>
<Box mb={2} display="flex" justifyContent="center">
<Image
src="/images/logos/icon-square-dark.svg"
alt="GridPilot"
width={40}
height={40}
fullHeight
style={{ width: 'auto' }}
/>
</Box>
<Text size={{ base: 'xs', lg: 'sm' }} color="text-gray-300" block mb={{ base: 1, md: 2 }}>
<Text size="sm" color="text-gray-300" block mb={2} weight="bold" className="tracking-wide">
🏁 Built by a sim racer, for sim racers
</Text>
<Text size={{ base: 'xs', md: 'xs' }} color="text-gray-400" weight="light" maxWidth="2xl" mx="auto" block>
<Text size="xs" color="text-gray-500" weight="normal" maxWidth="2xl" mx="auto" block leading="relaxed">
Just a fellow racer tired of spreadsheets and chaos. GridPilot is my passion project to make league racing actually fun again.
</Text>
</Box>
@@ -51,50 +41,39 @@ export function Footer() {
<Box
display="flex"
justifyContent="center"
gap={{ base: 4, md: 6, lg: 8 }}
mb={{ base: 3, md: 6, lg: 8 }}
gap={8}
mb={12}
>
<Link
href={discordUrl}
variant="ghost"
size="xs"
hoverTextColor="text-neon-aqua"
transition
px={3}
py={2}
minHeight="44px"
minWidth="44px"
size="sm"
className="text-gray-400 hover:text-primary-accent transition-colors font-bold uppercase tracking-widest"
>
💬 Join Discord
💬 Discord
</Link>
<Link
href={xUrl}
variant="ghost"
size="xs"
color="text-gray-300"
hoverTextColor="text-neon-aqua"
transition
px={3}
py={2}
minHeight="44px"
minWidth="44px"
size="sm"
className="text-gray-400 hover:text-primary-accent transition-colors font-bold uppercase tracking-widest"
>
𝕏 Follow on X
𝕏 Twitter
</Link>
</Box>
{/* Development status */}
<Box
textAlign="center"
pt={{ base: 2, md: 4, lg: 6 }}
pt={8}
borderTop
borderColor="border-charcoal-outline"
borderColor="border-gray/30"
>
<Text size={{ base: 'xs', lg: 'sm' }} color="text-gray-500" block mb={{ base: 1, md: 2 }}>
<Text size="xs" color="text-gray-600" block mb={1} font="mono" uppercase letterSpacing="widest">
Early development Feedback welcome
</Text>
<Text size={{ base: 'xs', md: 'xs' }} color="text-gray-600" block>
Questions? Find me on Discord
<Text size="xs" color="text-gray-700" block font="mono">
&copy; {new Date().getFullYear()} GridPilot
</Text>
</Box>
</Box>

49
apps/website/ui/Glow.tsx Normal file
View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Box } from './Box';
interface GlowProps {
color?: 'primary' | 'aqua' | 'purple' | 'amber';
size?: 'sm' | 'md' | 'lg' | 'xl';
opacity?: number;
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
className?: string;
}
export function Glow({
color = 'primary',
size = 'md',
opacity = 0.1,
position = 'center',
className = ''
}: GlowProps) {
const colorMap = {
primary: 'from-primary-accent/20',
aqua: 'from-telemetry-aqua/20',
purple: 'from-purple-500/20',
amber: 'from-warning-amber/20',
};
const sizeMap = {
sm: 'w-64 h-64',
md: 'w-96 h-96',
lg: 'w-[32rem] h-[32rem]',
xl: 'w-[48rem] h-[48rem]',
};
const positionMap = {
'top-left': '-top-32 -left-32',
'top-right': '-top-32 -right-32',
'bottom-left': '-bottom-32 -left-32',
'bottom-right': '-bottom-32 -right-32',
'center': 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
};
return (
<Box
position="absolute"
className={`${sizeMap[size]} ${positionMap[position]} bg-radial-gradient ${colorMap[color]} to-transparent blur-[100px] pointer-events-none ${className}`}
style={{ opacity }}
zIndex={0}
/>
);
}

View File

@@ -7,7 +7,7 @@ interface HeaderProps {
export function Header({ children }: HeaderProps) {
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-deep-graphite/80 backdrop-blur-sm border-b border-white/5">
<header className="fixed top-0 left-0 right-0 z-50 bg-graphite-black/80 backdrop-blur-md border-b border-border-gray/50">
<Container>
{children}
</Container>

View File

@@ -26,12 +26,12 @@ export function Heading({ level, children, icon, groupHoverColor, truncate, font
const Tag = `h${level}` as ElementType;
const levelClasses = {
1: 'text-3xl md:text-4xl font-bold text-white',
2: 'text-xl font-semibold text-white',
3: 'text-lg font-semibold text-white',
4: 'text-base font-semibold text-white',
5: 'text-sm font-semibold text-white',
6: 'text-xs font-semibold text-white',
1: 'text-3xl md:text-4xl font-bold text-white tracking-tight',
2: 'text-xl md:text-2xl font-bold text-white tracking-tight',
3: 'text-lg font-bold text-white tracking-tight',
4: 'text-base font-bold text-white tracking-tight',
5: 'text-sm font-bold text-white tracking-tight uppercase tracking-wider',
6: 'text-xs font-bold text-white tracking-tight uppercase tracking-widest',
};
const weightClasses = {

View File

@@ -12,15 +12,15 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className = '', variant = 'default', errorMessage, icon, label, ...props }, ref) => {
const baseClasses = 'px-3 py-2 border rounded-lg text-white bg-deep-graphite focus:outline-none focus:border-primary-blue transition-colors w-full';
const variantClasses = (variant === 'error' || errorMessage) ? 'border-racing-red' : 'border-charcoal-outline';
const baseClasses = 'px-3 py-2 border rounded-sm text-white bg-graphite-black focus:outline-none focus:border-primary-accent transition-all duration-150 ease-smooth w-full text-sm placeholder:text-gray-600';
const variantClasses = (variant === 'error' || errorMessage) ? 'border-critical-red' : 'border-border-gray';
const iconClasses = icon ? 'pl-10' : '';
const classes = `${baseClasses} ${variantClasses} ${iconClasses} ${className}`;
return (
<Stack gap={1.5} fullWidth>
{label && (
<Text as="label" size="sm" weight="medium" color="text-gray-300">
<Text as="label" size="xs" weight="bold" color="text-gray-500" className="uppercase tracking-wider">
{label}
</Text>
)}
@@ -34,13 +34,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
zIndex={10}
display="flex"
center
className="text-gray-500"
>
{icon}
</Box>
)}
<input ref={ref} className={classes} {...props} />
{errorMessage && (
<Text size="xs" color="text-error-red" block mt={1}>
<Text size="xs" color="text-critical-red" block mt={1}>
{errorMessage}
</Text>
)}
@@ -50,4 +51,4 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
}
);
Input.displayName = 'Input';
Input.displayName = 'Input';

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Box } from '@/ui/Box';
export function FeatureItem({ text }: { text: string }) {
return (
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/40" borderColor="border-gray/50" className="group hover:border-primary-accent/30 transition-colors">
<Stack direction="row" align="center" gap={4}>
<Box w="1" h="4" bg="primary-accent" className="group-hover:h-6 transition-all" />
<Text color="text-gray-300" leading="relaxed" weight="normal" size="sm" className="tracking-wide">
{text}
</Text>
</Stack>
</Surface>
);
}
export function ResultItem({ text, color }: { text: string, color: string }) {
return (
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/40" borderColor="border-gray/50" className="group hover:border-primary-accent/30 transition-colors">
<Stack direction="row" align="center" gap={4}>
<Box w="1" h="4" style={{ backgroundColor: color }} className="group-hover:h-6 transition-all" />
<Text color="text-gray-300" leading="relaxed" weight="normal" size="sm" className="tracking-wide">
{text}
</Text>
</Stack>
</Surface>
);
}
export function StepItem({ step, text }: { step: number, text: string }) {
return (
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/40" borderColor="border-gray/50" className="group hover:border-primary-accent/30 transition-colors">
<Stack direction="row" align="center" gap={4}>
<Box w="8" h="8" display="flex" center border borderColor="border-gray" className="group-hover:border-primary-accent/50 transition-colors">
<Text weight="bold" size="xs" color="text-primary-accent" font="mono">{step.toString().padStart(2, '0')}</Text>
</Box>
<Text color="text-gray-300" leading="relaxed" weight="normal" size="sm" className="tracking-wide">
{text}
</Text>
</Stack>
</Surface>
);
}

View File

@@ -5,6 +5,7 @@ import { Box } from './Box';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Text } from './Text';
import { Stack } from './Stack';
import { Image } from './Image';
import { PlaceholderImage } from './PlaceholderImage';
import { Calendar as LucideCalendar, ChevronRight as LucideChevronRight, Users as LucideUsers } from 'lucide-react';
@@ -60,13 +61,13 @@ export function LeagueCard({
<Box
position="relative"
h="full"
rounded="xl"
bg="bg-iron-gray"
rounded="none"
bg="panel-gray/40"
border
borderColor="border-charcoal-outline"
borderColor="border-gray/50"
overflow="hidden"
transition
className="hover:border-primary-blue/50 hover:shadow-[0_0_30px_rgba(25,140,255,0.15)] hover:bg-iron-gray/80"
className="hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300"
>
{/* Cover Image */}
<Box position="relative" h="32" overflow="hidden">
@@ -76,10 +77,10 @@ export function LeagueCard({
fullWidth
fullHeight
objectFit="cover"
className="transition-transform duration-300 group-hover:scale-105"
className="transition-transform duration-500 group-hover:scale-105 opacity-60"
/>
{/* Gradient Overlay */}
<Box position="absolute" inset="0" bg="bg-gradient-to-t from-iron-gray via-iron-gray/60 to-transparent" />
<Box position="absolute" inset="0" bg="linear-gradient(to top, #0C0D0F, transparent)" />
{/* Badges - Top Left */}
<Box position="absolute" top="3" left="3" display="flex" alignItems="center" gap={2}>
@@ -93,7 +94,7 @@ export function LeagueCard({
{/* Logo */}
<Box position="absolute" left="4" bottom="-6" zIndex={10}>
<Box w="12" h="12" rounded="lg" overflow="hidden" border borderColor="border-iron-gray" bg="bg-deep-graphite" shadow="xl">
<Box w="12" h="12" rounded="none" overflow="hidden" border borderColor="border-gray/50" bg="graphite-black" shadow="xl">
{logoUrl ? (
<Image
src={logoUrl}
@@ -114,34 +115,37 @@ export function LeagueCard({
{/* Content */}
<Box pt={8} px={4} pb={4} display="flex" flexDirection="col" fullHeight>
{/* Title & Description */}
<Heading level={3} className="line-clamp-1 group-hover:text-primary-blue transition-colors" mb={1}>
{name}
</Heading>
<Text size="xs" color="text-gray-500" lineClamp={2} mb={3} style={{ height: '2rem' }} block>
<Stack direction="row" align="center" gap={2} mb={1}>
<Box w="1" h="4" bg="primary-accent" />
<Heading level={3} fontSize="lg" weight="bold" className="line-clamp-1 group-hover:text-primary-accent transition-colors tracking-tight">
{name}
</Heading>
</Stack>
<Text size="xs" color="text-gray-500" lineClamp={2} mb={4} style={{ height: '2.5rem' }} block leading="relaxed">
{description || 'No description available'}
</Text>
{/* Stats Row */}
<Box display="flex" alignItems="center" gap={3} mb={3}>
<Box display="flex" alignItems="center" gap={3} mb={4}>
{/* Primary Slots (Drivers/Teams/Nations) */}
<Box flexGrow={1}>
<Box display="flex" alignItems="center" justifyContent="between" mb={1}>
<Text size="xs" color="text-gray-500">{slotLabel}</Text>
<Text size="xs" color="text-gray-400">
<Box display="flex" alignItems="center" justifyContent="between" mb={1.5}>
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">{slotLabel}</Text>
<Text size="xs" color="text-gray-400" font="mono">
{usedSlots}/{maxSlots || '∞'}
</Text>
</Box>
<Box h="1.5" rounded="full" bg="bg-charcoal-outline" overflow="hidden">
<Box h="1" rounded="none" bg="border-gray/30" overflow="hidden">
<Box
h="full"
rounded="full"
rounded="none"
transition
bg={
fillPercentage >= 90
? 'bg-warning-amber'
? 'warning-amber'
: fillPercentage >= 70
? 'bg-primary-blue'
: 'bg-performance-green'
? 'primary-accent'
: 'success-green'
}
style={{ width: `${Math.min(fillPercentage, 100)}%` }}
/>
@@ -150,35 +154,25 @@ export function LeagueCard({
{/* Open Slots Badge */}
{hasOpenSlots && (
<Box display="flex" alignItems="center" gap={1} px={2} py={1} rounded="lg" bg="bg-neon-aqua/10" border borderColor="border-neon-aqua/20">
<Box w="1.5" h="1.5" rounded="full" bg="bg-neon-aqua" animate="pulse" />
<Text size="xs" color="text-neon-aqua" weight="medium">
{openSlotsCount} open
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="none" bg="primary-accent/5" border borderColor="primary-accent/20">
<Box w="1.5" h="1.5" rounded="full" bg="primary-accent" className="animate-pulse" />
<Text size="xs" color="text-primary-accent" weight="bold" className="uppercase tracking-tighter">
{openSlotsCount} OPEN
</Text>
</Box>
)}
</Box>
{/* Driver count for team leagues */}
{isTeamLeague && (
<Box display="flex" alignItems="center" gap={2} mb={3}>
<Icon icon={LucideUsers} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500">
{usedDriverSlots ?? 0}/{maxDrivers ?? '∞'} drivers
</Text>
</Box>
)}
{/* Spacer to push footer to bottom */}
<Box flexGrow={1} />
{/* Footer Info */}
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop style={{ borderColor: 'rgba(38, 38, 38, 0.5)' }} mt="auto">
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-gray/30" mt="auto">
<Box display="flex" alignItems="center" gap={3}>
{timingSummary && (
<Box display="flex" alignItems="center" gap={1}>
<Box display="flex" alignItems="center" gap={2}>
<Icon icon={LucideCalendar} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500">
<Text size="xs" color="text-gray-500" font="mono">
{timingSummary.split('•')[1]?.trim() || timingSummary}
</Text>
</Box>
@@ -186,8 +180,8 @@ export function LeagueCard({
</Box>
{/* View Arrow */}
<Box display="flex" alignItems="center" gap={1} className="group-hover:text-primary-blue transition-colors">
<Text size="xs" color="text-gray-500">View</Text>
<Box display="flex" alignItems="center" gap={1} className="group-hover:text-primary-accent transition-colors">
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">VIEW</Text>
<Icon icon={LucideChevronRight} size={3} color="text-gray-500" className="transition-transform group-hover:translate-x-0.5" />
</Box>
</Box>

View File

@@ -34,8 +34,8 @@ export function Link({
const baseClasses = 'inline-flex items-center transition-colors';
const variantClasses = {
primary: 'text-primary-blue hover:text-primary-blue/80',
secondary: 'text-purple-300 hover:text-purple-400',
primary: 'text-primary-accent hover:text-primary-accent/80',
secondary: 'text-telemetry-aqua hover:text-telemetry-aqua/80',
ghost: 'text-gray-400 hover:text-gray-300'
};

View File

@@ -5,5 +5,5 @@ interface MainContentProps {
}
export function MainContent({ children }: MainContentProps) {
return <div className="pt-16">{children}</div>;
return <div className="pt-16 md:pt-20">{children}</div>;
}

View File

@@ -28,10 +28,10 @@ export function Section({
}: SectionProps) {
const variantClasses = {
default: '',
card: 'bg-iron-gray rounded-lg p-6 border border-charcoal-outline',
highlight: 'bg-gradient-to-r from-blue-900/20 to-blue-700/10 rounded-lg p-6 border border-blue-500/30',
dark: 'bg-iron-gray',
light: 'bg-charcoal-outline'
card: 'bg-panel-gray rounded-none p-6 border border-border-gray',
highlight: 'bg-gradient-to-r from-primary-accent/10 to-transparent rounded-none p-6 border border-primary-accent/30',
dark: 'bg-graphite-black',
light: 'bg-panel-gray'
};
const classes = [

View File

@@ -17,7 +17,7 @@ export function Surface<T extends ElementType = 'div'>({
as,
children,
variant = 'default',
rounded = 'lg',
rounded = 'none',
border = false,
padding = 0,
className = '',
@@ -26,16 +26,16 @@ export function Surface<T extends ElementType = 'div'>({
...props
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>) {
const variantClasses: Record<string, string> = {
default: 'bg-iron-gray',
muted: 'bg-iron-gray/50',
dark: 'bg-deep-graphite',
glass: 'bg-deep-graphite/60 backdrop-blur-md',
'gradient-blue': 'bg-gradient-to-br from-primary-blue/20 via-iron-gray/80 to-deep-graphite',
'gradient-gold': 'bg-gradient-to-br from-yellow-600/20 via-iron-gray/80 to-deep-graphite',
'gradient-purple': 'bg-gradient-to-br from-purple-600/20 via-iron-gray/80 to-deep-graphite',
'gradient-green': 'bg-gradient-to-br from-green-600/20 via-iron-gray/80 to-deep-graphite',
'discord': 'bg-gradient-to-b from-deep-graphite to-iron-gray',
'discord-inner': 'bg-gradient-to-br from-iron-gray via-deep-graphite to-iron-gray'
default: 'bg-panel-gray',
muted: 'bg-panel-gray/40',
dark: 'bg-graphite-black',
glass: 'bg-graphite-black/60 backdrop-blur-md',
'gradient-blue': 'bg-gradient-to-br from-primary-accent/10 via-panel-gray/80 to-graphite-black',
'gradient-gold': 'bg-gradient-to-br from-warning-amber/10 via-panel-gray/80 to-graphite-black',
'gradient-purple': 'bg-gradient-to-br from-purple-600/10 via-panel-gray/80 to-graphite-black',
'gradient-green': 'bg-gradient-to-br from-success-green/10 via-panel-gray/80 to-graphite-black',
'discord': 'bg-gradient-to-b from-graphite-black to-panel-gray',
'discord-inner': 'bg-gradient-to-br from-panel-gray via-graphite-black to-panel-gray'
};
const shadowClasses: Record<string, string> = {
@@ -72,7 +72,7 @@ export function Surface<T extends ElementType = 'div'>({
const classes = [
variantClasses[variant],
roundedClasses[rounded],
border ? 'border border-charcoal-outline' : '',
border ? 'border border-border-gray' : '',
paddingClasses[padding] || 'p-0',
shadowClasses[shadow],
display ? display : '',

View File

@@ -8,8 +8,8 @@ interface TableProps extends HTMLAttributes<HTMLTableElement> {
export function Table({ children, className = '', ...props }: TableProps) {
return (
<Box overflow="auto">
<table className={`w-full ${className}`} {...props}>
<Box overflow="auto" className="border border-border-gray rounded-sm">
<table className={`w-full border-collapse text-left ${className}`} {...props}>
{children}
</table>
</Box>
@@ -22,7 +22,7 @@ interface TableHeadProps extends HTMLAttributes<HTMLTableSectionElement> {
export function TableHead({ children, ...props }: TableHeadProps) {
return (
<thead {...props}>
<thead className="bg-graphite-black border-b border-border-gray" {...props}>
{children}
</thead>
);
@@ -34,7 +34,7 @@ interface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {
export function TableBody({ children, ...props }: TableBodyProps) {
return (
<tbody {...props}>
<tbody className="divide-y divide-border-gray/50" {...props}>
{children}
</tbody>
);
@@ -47,8 +47,8 @@ interface TableRowProps extends BoxProps<'tr'> {
}
export function TableRow({ children, className = '', clickable = false, variant = 'default', ...props }: TableRowProps) {
const baseClasses = 'border-b border-charcoal-outline/50 transition-colors';
const variantClasses = variant === 'highlight' ? 'bg-primary-blue/5' : '';
const baseClasses = 'transition-colors duration-150 ease-smooth';
const variantClasses = variant === 'highlight' ? 'bg-primary-accent/5' : 'hover:bg-white/[0.02]';
const classes = [
baseClasses,
variantClasses,
@@ -68,7 +68,7 @@ interface TableHeaderProps extends BoxProps<'th'> {
}
export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
const baseClasses = 'py-3 px-4 text-xs font-medium text-gray-400 uppercase';
const baseClasses = 'py-2.5 px-4 text-[11px] font-bold text-gray-500 uppercase tracking-wider';
const classes = [baseClasses, className].filter(Boolean).join(' ');
return (
@@ -83,7 +83,7 @@ interface TableCellProps extends BoxProps<'td'> {
}
export function TableCell({ children, className = '', ...props }: TableCellProps) {
const baseClasses = 'py-3 px-4';
const baseClasses = 'py-3 px-4 text-sm text-gray-300';
const classes = [baseClasses, className].filter(Boolean).join(' ');
return (

View File

@@ -47,53 +47,55 @@ export function TeamCard({
onClick,
}: TeamCardProps) {
return (
<Box onClick={onClick} h="full" cursor={onClick ? 'pointer' : 'default'}>
<Card h="full" p={0} display="flex" flexDirection="col" overflow="hidden">
<Box onClick={onClick} h="full" cursor={onClick ? 'pointer' : 'default'} className="group">
<Card h="full" p={0} display="flex" flexDirection="col" overflow="hidden" className="bg-panel-gray/40 border-border-gray/50 hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300">
{/* Header with Logo */}
<Box p={4} pb={0}>
<Box p={5} pb={0}>
<Stack direction="row" align="start" gap={4}>
{/* Logo */}
<Box
w="14"
h="14"
rounded="lg"
bg="bg-deep-graphite"
w="16"
h="16"
rounded="none"
bg="graphite-black"
display="flex"
center
overflow="hidden"
border
borderColor="border-charcoal-outline"
borderColor="border-gray/50"
className="relative"
>
{logo ? (
<Image
src={logo}
alt={name}
width={56}
height={56}
width={64}
height={64}
fullWidth
fullHeight
objectFit="cover"
/>
) : (
<PlaceholderImage size={56} />
<PlaceholderImage size={64} />
)}
<Box position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="primary-accent/30" />
</Box>
{/* Title & Badges */}
<Box flexGrow={1} minWidth="0">
<Stack direction="row" align="start" justify="between" gap={2}>
<Heading level={4}>
<Heading level={4} weight="bold" fontSize="lg" className="tracking-tight group-hover:text-primary-accent transition-colors">
{name}
</Heading>
{isRecruiting && (
<Badge variant="success" icon={UserPlus}>
Recruiting
<Badge variant="success" size="xs">
RECRUITING
</Badge>
)}
</Stack>
{/* Performance Level & Category */}
<Stack direction="row" align="center" gap={2} wrap mt={1.5}>
<Stack direction="row" align="center" gap={2} wrap mt={2}>
{performanceBadge}
{specializationContent}
{categoryBadge}
@@ -103,48 +105,43 @@ export function TeamCard({
</Box>
{/* Content */}
<Box p={4} display="flex" flexDirection="col" flexGrow={1}>
<Box p={5} display="flex" flexDirection="col" flexGrow={1}>
{/* Description */}
<Text
size="xs"
color="text-gray-500"
mb={3}
mb={4}
lineClamp={2}
block
leading="relaxed"
style={{ height: '2.5rem' }}
>
{description || 'No description available'}
</Text>
{/* Region & Languages */}
{(region || languagesContent) && (
<Stack direction="row" align="center" gap={2} wrap mb={3}>
<Stack direction="row" align="center" gap={2} wrap mb={4}>
{region && (
<Box
display="flex"
alignItems="center"
gap={1.5}
gap={2}
px={2}
py={1}
rounded="md"
bg="bg-iron-gray/50"
rounded="none"
bg="panel-gray/20"
border
style={{ borderColor: 'rgba(38, 38, 38, 0.3)' }}
borderColor="border-gray/30"
>
<Icon icon={Globe} size={3} color="var(--neon-aqua)" />
<Text size="xs" color="text-gray-400">{region}</Text>
<Icon icon={Globe} size={3} color="text-primary-accent" />
<Text size="xs" color="text-gray-400" weight="bold" className="uppercase tracking-widest">{region}</Text>
</Box>
)}
{languagesContent}
</Stack>
)}
{/* Stats Grid */}
{statsContent && (
<Box display="grid" gridCols={3} gap={2} mb={4}>
{statsContent}
</Box>
)}
{/* Spacer */}
<Box flexGrow={1} />
@@ -153,21 +150,21 @@ export function TeamCard({
display="flex"
alignItems="center"
justifyContent="between"
pt={3}
pt={4}
borderTop
style={{ borderColor: 'rgba(38, 38, 38, 0.5)' }}
borderColor="border-gray/30"
mt="auto"
>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Users} size={3} color="var(--text-gray-600)" />
<Text size="xs" color="text-gray-500">
{memberCount} {memberCount === 1 ? 'member' : 'members'}
<Icon icon={Users} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500" font="mono">
{memberCount} {memberCount === 1 ? 'MEMBER' : 'MEMBERS'}
</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Text size="xs" color="text-gray-500">View</Text>
<Icon icon={ChevronRight} size={3} color="var(--text-gray-600)" />
<Stack direction="row" align="center" gap={1} className="group-hover:text-primary-accent transition-colors">
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">VIEW</Text>
<Icon icon={ChevronRight} size={3} color="text-gray-500" className="transition-transform group-hover:translate-x-0.5" />
</Stack>
</Box>
</Box>

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Box } from './Box';
interface TelemetryLineProps {
color?: 'primary' | 'aqua' | 'amber' | 'green' | 'red';
height?: number | string;
animate?: boolean;
opacity?: number;
className?: string;
}
export function TelemetryLine({
color = 'primary',
height = '2px',
animate = false,
opacity = 1,
className = ''
}: TelemetryLineProps) {
const colorMap = {
primary: 'bg-primary-accent',
aqua: 'bg-telemetry-aqua',
amber: 'bg-warning-amber',
green: 'bg-success-green',
red: 'bg-critical-red',
};
return (
<Box
height={height}
fullWidth
className={`${colorMap[color]} ${animate ? 'animate-pulse' : ''} ${className}`}
style={{
opacity,
boxShadow: `0 0 8px ${
color === 'primary' ? '#198CFF' :
color === 'aqua' ? '#4ED4E0' :
color === 'amber' ? '#FFBE4D' :
color === 'green' ? '#6FE37A' : '#E35C5C'
}4D`
}}
/>
);
}

View File

@@ -24,30 +24,35 @@ export function UpcomingRaceItem({
return (
<Surface
variant="muted"
padding={3}
rounded="lg"
padding={4}
rounded="none"
border
borderColor="border-gray/30"
className="hover:border-primary-accent/30 transition-colors bg-panel-gray/20 group"
>
<Text color="text-white" weight="medium" block>
{track}
</Text>
<Text size="sm" color="text-gray-400" block>
{car}
</Text>
<Stack direction="row" align="center" gap={2} mt={1}>
<Text size="xs" color="text-gray-500">
{formattedDate}
</Text>
<Text size="xs" color="text-gray-500">
</Text>
<Text size="xs" color="text-gray-500">
{formattedTime}
</Text>
<Stack direction="row" align="center" gap={4}>
<Box w="1" h="8" bg="primary-accent" opacity={0.3} className="group-hover:opacity-100 transition-opacity" />
<Box flexGrow={1}>
<Text color="text-white" weight="bold" block className="tracking-tight">
{track}
</Text>
<Text size="xs" color="text-gray-500" block weight="medium" className="uppercase tracking-widest mt-0.5">
{car}
</Text>
</Box>
<Stack align="end" gap={1}>
<Text size="xs" color="text-gray-400" font="mono" weight="bold">
{formattedDate}
</Text>
<Text size="xs" color="text-gray-600" font="mono">
{formattedTime}
</Text>
</Stack>
</Stack>
{isMyLeague && (
<Box mt={1}>
<Badge variant="success">
Your League
<Box mt={3} display="flex" justifyContent="end">
<Badge variant="success" size="xs">
YOUR LEAGUE
</Badge>
</Box>
)}