website refactor
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'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'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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
© {new Date().getFullYear()} GridPilot
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
49
apps/website/ui/Glow.tsx
Normal file
49
apps/website/ui/Glow.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
46
apps/website/ui/LandingItems.tsx
Normal file
46
apps/website/ui/LandingItems.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 : '',
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
43
apps/website/ui/TelemetryLine.tsx
Normal file
43
apps/website/ui/TelemetryLine.tsx
Normal 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`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user