From 4d5ce9bfd692523028b13aa1734ec1d2a5ef80c7 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 17 Jan 2026 02:32:34 +0100 Subject: [PATCH] website refactor --- .../components/landing/AlternatingSection.tsx | 100 +++--- .../components/landing/DiscoverySection.tsx | 123 +++++++ apps/website/components/landing/FAQ.tsx | 54 +-- .../components/landing/FeatureGrid.tsx | 64 ++-- .../components/landing/LandingHero.tsx | 202 +++++------ .../components/landing/LandingItems.tsx | 53 --- .../components/layout/HeaderContent.tsx | 41 ++- .../mockups/CareerProgressionMockup.tsx | 79 +++-- .../mockups/CompanionAutomationMockup.tsx | 153 ++++----- .../mockups/DriverProfileMockup.tsx | 175 +++------- .../mockups/LeagueDiscoveryMockup.tsx | 237 ++++--------- .../components/mockups/LeagueHomeMockup.tsx | 135 +++----- .../components/mockups/MockupStack.tsx | 16 +- .../mockups/ProtestWorkflowMockup.tsx | 113 ++---- .../components/mockups/RaceHistoryMockup.tsx | 140 ++++---- .../components/mockups/SimPlatformMockup.tsx | 109 +++--- .../mockups/StandingsTableMockup.tsx | 142 ++++---- .../mockups/TeamCompetitionMockup.tsx | 258 ++++---------- .../components/mockups/WorkflowMockup.tsx | 48 +-- .../notifications/ModalNotification.tsx | 157 ++++----- apps/website/tailwind.config.js | 46 ++- apps/website/templates/HomeTemplate.tsx | 321 ++++++------------ apps/website/ui/Badge.tsx | 16 +- apps/website/ui/Button.tsx | 25 +- apps/website/ui/Card.tsx | 13 +- apps/website/ui/DecorativeBlur.tsx | 8 +- apps/website/ui/DiscordCTA.tsx | 157 ++++----- apps/website/ui/Footer.tsx | 69 ++-- apps/website/ui/Glow.tsx | 49 +++ apps/website/ui/Header.tsx | 2 +- apps/website/ui/Heading.tsx | 12 +- apps/website/ui/Input.tsx | 11 +- apps/website/ui/LandingItems.tsx | 46 +++ apps/website/ui/LeagueCard.tsx | 72 ++-- apps/website/ui/Link.tsx | 4 +- apps/website/ui/MainContent.tsx | 2 +- apps/website/ui/Section.tsx | 8 +- apps/website/ui/Surface.tsx | 24 +- apps/website/ui/Table.tsx | 16 +- apps/website/ui/TeamCard.tsx | 75 ++-- apps/website/ui/TelemetryLine.tsx | 43 +++ apps/website/ui/UpcomingRaceItem.tsx | 47 +-- docs/THEME.md | 199 +++++------ 43 files changed, 1642 insertions(+), 2022 deletions(-) create mode 100644 apps/website/components/landing/DiscoverySection.tsx delete mode 100644 apps/website/components/landing/LandingItems.tsx create mode 100644 apps/website/ui/Glow.tsx create mode 100644 apps/website/ui/LandingItems.tsx create mode 100644 apps/website/ui/TelemetryLine.tsx diff --git a/apps/website/components/landing/AlternatingSection.tsx b/apps/website/components/landing/AlternatingSection.tsx index 2491378e9..645c8b1ec 100644 --- a/apps/website/components/landing/AlternatingSection.tsx +++ b/apps/website/components/landing/AlternatingSection.tsx @@ -1,10 +1,9 @@ - - import { useParallax } from "@/hooks/useScrollProgress"; import { Box } from '@/ui/Box'; import { Container } from '@/ui/Container'; import { Heading } from '@/ui/Heading'; import { Text } from '@/ui/Text'; +import { Stack } from '@/ui/Stack'; import { useRef } from 'react'; interface AlternatingSectionProps { @@ -25,8 +24,7 @@ export function AlternatingSection({ backgroundVideo }: AlternatingSectionProps) { const sectionRef = useRef(null); - - const bgParallax = useParallax(sectionRef, 0.2); + const bgParallax = useParallax(sectionRef, 0.1); return ( {backgroundVideo && ( - <> + - {/* Racing red accent for sections with background videos */} - - + {/* Dark overlay to ensure readability */} + + )} {backgroundImage && !backgroundVideo && ( - <> + - {/* Racing red accent for sections with background images */} - - + {/* Dark overlay to ensure readability */} + + )} - {/* Carbon fiber texture on sections without images or videos */} - {!backgroundImage && !backgroundVideo && ( - - )} - - {/* Checkered pattern accent */} - - - - {/* Text Content - Always first on mobile, respects layout on desktop */} + + {/* Text Content */} - - {heading} - - - - {description} - + + + + {heading} + + + + {typeof description === 'string' ? ( + {description} + ) : ( + description + )} - {/* Mockup - Always second on mobile, respects layout on desktop */} + {/* Mockup */} {mockup} + {/* Decorative corner accents */} + + ); -} \ No newline at end of file +} diff --git a/apps/website/components/landing/DiscoverySection.tsx b/apps/website/components/landing/DiscoverySection.tsx new file mode 100644 index 000000000..a31ba2517 --- /dev/null +++ b/apps/website/components/landing/DiscoverySection.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { routes } from '@/lib/routing/RouteConfig'; +import { Box } from '@/ui/Box'; +import { Button } from '@/ui/Button'; +import { Container } from '@/ui/Container'; +import { Grid } from '@/ui/Grid'; +import { Heading } from '@/ui/Heading'; +import { Link } from '@/ui/Link'; +import { Stack } from '@/ui/Stack'; +import { Text } from '@/ui/Text'; +import { LeagueCard } from '@/ui/LeagueCard'; +import { TeamCard } from '@/ui/TeamCard'; +import { UpcomingRaceItem } from '@/ui/UpcomingRaceItem'; +import { HomeViewData } from '@/templates/HomeTemplate'; + +interface DiscoverySectionProps { + viewData: HomeViewData; +} + +export function DiscoverySection({ viewData }: DiscoverySectionProps) { + return ( + + + + + + + Live Ecosystem + + + + Discover the Grid + + + Explore leagues, teams, and races that make up the GridPilot ecosystem. + + + + + {/* Top leagues */} + + + FEATURED LEAGUES + + View all + + + + {viewData.topLeagues.slice(0, 2).map((league) => ( + + ))} + + + + {/* Teams */} + + + TEAMS ON THE GRID + + Browse + + + + {viewData.teams.slice(0, 2).map(team => ( + + ))} + + + + {/* Upcoming races */} + + + UPCOMING RACES + + Schedule + + + {viewData.upcomingRaces.length === 0 ? ( + + + No races scheduled. + + + ) : ( + + {viewData.upcomingRaces.map(race => ( + + ))} + + )} + + + + + + ); +} diff --git a/apps/website/components/landing/FAQ.tsx b/apps/website/components/landing/FAQ.tsx index e93ff1d16..cb9a420e4 100644 --- a/apps/website/components/landing/FAQ.tsx +++ b/apps/website/components/landing/FAQ.tsx @@ -5,6 +5,7 @@ import { motion } from 'framer-motion'; import { Box } from '@/ui/Box'; import { Text } from '@/ui/Text'; import { Heading } from '@/ui/Heading'; +import { Stack } from '@/ui/Stack'; import { Icon } from '@/ui/Icon'; import { ChevronDown } from 'lucide-react'; @@ -47,27 +48,31 @@ function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) { transition={{ delay: index * 0.1 }} group > - + setIsOpen(!isOpen)} fullWidth - p={{ base: 2, md: 3, lg: 4 }} + p={{ base: 4, md: 6 }} textAlign="left" - rounded="lg" + rounded="none" minHeight="44px" + className="relative overflow-hidden" > - - - {faq.question} - + + + + + {faq.question} + + @@ -82,13 +87,13 @@ function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) { opacity: isOpen ? 1 : 0 }} transition={{ - height: { duration: 0.3, ease: [0.34, 1.56, 0.64, 1] }, - opacity: { duration: 0.2, ease: 'easeInOut' } + height: { duration: 0.2, ease: 'easeInOut' }, + opacity: { duration: 0.15, ease: 'easeInOut' } }} overflow="hidden" > - - + + {faq.answer} @@ -100,7 +105,7 @@ function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) { export function FAQ() { return ( - + {/* Background image with mask */} - {/* Racing red accent */} - - - - + + + + + Support & Information + + + Frequently Asked Questions - - + {faqs.map((faq, index) => ( ))} diff --git a/apps/website/components/landing/FeatureGrid.tsx b/apps/website/components/landing/FeatureGrid.tsx index 71d69272f..596ef8630 100644 --- a/apps/website/components/landing/FeatureGrid.tsx +++ b/apps/website/components/landing/FeatureGrid.tsx @@ -53,50 +53,64 @@ function FeatureCard({ feature, index }: { feature: typeof features[0], index: n display="flex" flexDirection="column" gap={6} - group + className="p-8 bg-panel-gray/40 border border-border-gray/50 rounded-none hover:border-primary-accent/30 transition-all duration-300 ease-smooth group relative overflow-hidden" > - - - - - - - + + + + - - - + + + + {feature.title} - + {feature.description} + {/* Subtle hover effect */} + ); } export function FeatureGrid() { return ( -
+
- - - + + + + + Engineered for Competition + + + Building for League Racing - - These features are in development. Join the community to help shape what gets built first + + Every feature is designed to reduce friction and increase immersion. Join our Discord to help shape the future of the platform. - - - {features.map((feature, index) => ( - - ))} - + + + {features.map((feature, index) => ( + + ))} + +
); -} \ No newline at end of file +} diff --git a/apps/website/components/landing/LandingHero.tsx b/apps/website/components/landing/LandingHero.tsx index b9648c8a4..dd169cead 100644 --- a/apps/website/components/landing/LandingHero.tsx +++ b/apps/website/components/landing/LandingHero.tsx @@ -1,4 +1,3 @@ - import { useRef } from 'react'; import { useParallax } from '@/hooks/useScrollProgress'; import { Box } from '@/ui/Box'; @@ -7,17 +6,13 @@ import { Container } from '@/ui/Container'; import { Heading } from '@/ui/Heading'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; +import { Glow } from '@/ui/Glow'; const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#'; -if (!process.env.NEXT_PUBLIC_DISCORD_URL) { - console.warn('NEXT_PUBLIC_DISCORD_URL is not set. Discord button will use "#" as fallback.'); -} - export function LandingHero() { const sectionRef = useRef(null); - - const bgParallax = useParallax(sectionRef, 0.3); + const bgParallax = useParallax(sectionRef, 0.2); return ( {/* Background image layer with parallax */} - {/* Racing red accent gradient */} - + {/* Robust gradient overlay */} + - {/* Racing stripes background */} - + - {/* Checkered pattern overlay */} - + - {/* Speed lines - left side */} - - - - - {/* Carbon fiber accent - bottom */} - - - {/* Radial gradient overlay with racing red accent */} - - - - - - League racing is incredible. What's missing is everything around it. - - - - If you've been in any league, you know the feeling: - - {/* Problem badges - mobile optimized */} - - {[ - 'Results scattered across Discord', - 'No long-term identity', - 'No career progression', - 'Forgotten after each season' - ].map((text) => ( - - × - {text} - - ))} + + + + + + Precision Racing Infrastructure + - - The ecosystem isn't built for this. + + Modern Motorsport
+ Infrastructure. +
+ + GridPilot gives your league racing a real home. Results, standings, teams, and career progression — engineered for precision and control. - - GridPilot gives your league racing a real home. - -
- +
+ + + + + + {/* Problem list - more professional */} + + {[ + { label: 'IDENTITY', text: 'Your racing career in one place', color: 'primary' }, + { label: 'AUTOMATION', text: 'No more manual session setup', color: 'aqua' }, + { label: 'PRECISION', text: 'Real-time results and standings', color: 'amber' }, + { label: 'COMMUNITY', text: 'Built for teams and leagues', color: 'green' } + ].map((item) => ( + + + + + {item.label} + + + + {item.text} + + + ))}
diff --git a/apps/website/components/landing/LandingItems.tsx b/apps/website/components/landing/LandingItems.tsx deleted file mode 100644 index 4efecb7fa..000000000 --- a/apps/website/components/landing/LandingItems.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import React from 'react'; -import { Check } from 'lucide-react'; -import { Stack } from '@/ui/Stack'; -import { Text } from '@/ui/Text'; -import { Surface } from '@/ui/Surface'; -import { Icon } from '@/ui/Icon'; - -export function FeatureItem({ text }: { text: string }) { - return ( - - - - - - - {text} - - - - ); -} - -export function ResultItem({ text, color }: { text: string, color: string }) { - return ( - - - - - - - {text} - - - - ); -} - -export function StepItem({ step, text }: { step: number, text: string }) { - return ( - - - - {step} - - - {text} - - - - ); -} diff --git a/apps/website/components/layout/HeaderContent.tsx b/apps/website/components/layout/HeaderContent.tsx index f9b619be1..43e49aabb 100644 --- a/apps/website/components/layout/HeaderContent.tsx +++ b/apps/website/components/layout/HeaderContent.tsx @@ -5,21 +5,34 @@ import { Text } from '@/ui/Text'; export function HeaderContent() { return ( -
-
- - GridPilot +
+
+ +
+ GridPilot +
+
- - Making league racing less chaotic - +
+
+ + Motorsport Infrastructure + +
+
+ +
+
+ Status: + Operational +
); diff --git a/apps/website/components/mockups/CareerProgressionMockup.tsx b/apps/website/components/mockups/CareerProgressionMockup.tsx index 17c6814c5..1b47152fa 100644 --- a/apps/website/components/mockups/CareerProgressionMockup.tsx +++ b/apps/website/components/mockups/CareerProgressionMockup.tsx @@ -25,7 +25,7 @@ export function CareerProgressionMockup() { // Simple mobile version - just the essence if (isMobile) { return ( - + ( - - {stat.value} + + {stat.value} {stat.label} @@ -61,11 +63,11 @@ export function CareerProgressionMockup() { {/* Single elegant season card */} - + - GT3 Championship - - P2 + GT3 Championship + + P2 @@ -78,7 +80,7 @@ export function CareerProgressionMockup() { // Desktop version - more detailed return ( - + {/* Driver Header */} - - + + 🏎️ + - Your Racing Identity + Your Racing Identity Multi-league profile Career tracking @@ -118,7 +123,7 @@ export function CareerProgressionMockup() { {/* Career Stats */} - Career Overview + Career Overview {[ { label: 'Wins', value: '24' }, @@ -128,19 +133,19 @@ export function CareerProgressionMockup() { - {stat.value} - {stat.label} + {stat.value} + {stat.label} ))} @@ -148,7 +153,7 @@ export function CareerProgressionMockup() { {/* Season Timeline */} - Season History + Season History {[ { league: 'GT3 Championship', season: 'S3', position: 'P2', points: '248' }, @@ -161,30 +166,30 @@ export function CareerProgressionMockup() { display="flex" alignItems="center" gap={{ base: 1.5, sm: 2, md: 3 }} - bg="bg-iron-gray" - rounded="lg" + bg="panel-gray/20" + rounded="none" p={{ base: 1.5, sm: 2, md: 3 }} border - borderColor="border-charcoal-outline" + borderColor="border-gray/30" whileHover={shouldReduceMotion ? {} : { x: 4, - boxShadow: '0 2px 12px rgba(25,140,255,0.2)', + borderColor: '#198CFF50', transition: { duration: 0.15 } }} > - + 🏁 - {season.league} - Season complete + {season.league} + Season complete - - {season.position} + + {season.position} - - {season.points} + + {season.points} @@ -194,18 +199,18 @@ export function CareerProgressionMockup() { {/* Multi-League Badge */} - + {[1, 2, 3].map((i) => ( - + 🏆 ))} - Active in 3 leagues across seasons + Active in 3 leagues across seasons diff --git a/apps/website/components/mockups/CompanionAutomationMockup.tsx b/apps/website/components/mockups/CompanionAutomationMockup.tsx index 0c3bbc76e..d48c10e38 100644 --- a/apps/website/components/mockups/CompanionAutomationMockup.tsx +++ b/apps/website/components/mockups/CompanionAutomationMockup.tsx @@ -2,12 +2,10 @@ import { motion, useReducedMotion } from 'framer-motion'; import { useState, useEffect } from 'react'; -import { Check } from 'lucide-react'; import { Box } from '@/ui/Box'; import { Text } from '@/ui/Text'; import { Stack } from '@/ui/Stack'; import { Heading } from '@/ui/Heading'; -import { Icon } from '@/ui/Icon'; export function CompanionAutomationMockup() { const shouldReduceMotion = useReducedMotion(); @@ -23,7 +21,7 @@ export function CompanionAutomationMockup() { // Simple mobile version - just the essence of automation if (isMobile) { return ( - + {/* Simple progress indicator */} - + - + + - Creating Session - Automated + Creating Session + AUTOMATED - + - - One Click + + One Click @@ -86,7 +86,7 @@ export function CompanionAutomationMockup() { // Desktop version - richer with more automation steps return ( - + - - + + + + - GridPilot Companion - Automated Session Creator + GridPilot Companion + AUTOMATED SESSION CREATOR @@ -118,26 +120,25 @@ export function CompanionAutomationMockup() { as={motion.div} variants={{ hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0 } }} position="relative" - bg="bg-charcoal-outline" - rounded="lg" + bg="panel-gray/40" + rounded="none" p={{ base: 3, md: 4, lg: 5 }} border - borderWidth="2px" - borderColor="border-primary-blue/40" + borderWidth="1px" + borderColor="border-gray/50" overflow="hidden" > {/* Browser Window Mockup */} - - - - - + + + + + members.iracing.com @@ -169,54 +170,53 @@ export function CompanionAutomationMockup() { as={motion.div} h={{ base: 7, md: 8, lg: 9 }} w={{ base: 7, md: 8, lg: 9 }} - rounded="full" + rounded="none" display="flex" alignItems="center" justifyContent="center" flexShrink={0} border - borderWidth="2px" + borderWidth="1px" bg={ step.status === 'Complete' - ? 'bg-performance-green/40' + ? 'success-green/10' : step.status === 'Running' - ? 'bg-primary-blue/40' - : 'bg-charcoal-outline' + ? 'primary-accent/10' + : 'transparent' } borderColor={ step.status === 'Complete' - ? 'border-performance-green/60' + ? 'success-green/40' : step.status === 'Running' - ? 'border-primary-blue/60' - : 'border-white/20' + ? 'primary-accent/40' + : 'border-gray/30' } animate={shouldReduceMotion ? {} : step.status === 'Running' ? { - scale: [1, 1.15, 1], opacity: [0.4, 1, 0.4] } : {}} transition={{ duration: 1.5, repeat: Infinity }} > {step.status === 'Complete' && ( - + )} {step.status === 'Running' && ( - + )} {step.status === 'Pending' && ( - + )} - {step.label} - {step.detail} + {step.label} + {step.detail.toUpperCase()} {step.status !== 'Pending' && ( - + - Running + Running @@ -278,32 +275,36 @@ export function CompanionAutomationMockup() { display="flex" flexDirection="col" alignItems="center" - gap={{ base: 2, md: 3 }} + gap={3} > - Create Session + Create Session + + One click. All fields automated. diff --git a/apps/website/components/mockups/DriverProfileMockup.tsx b/apps/website/components/mockups/DriverProfileMockup.tsx index c0fb9851c..dca035b27 100644 --- a/apps/website/components/mockups/DriverProfileMockup.tsx +++ b/apps/website/components/mockups/DriverProfileMockup.tsx @@ -11,7 +11,10 @@ export function DriverProfileMockup() { const [isMobile, setIsMobile] = useState(false); useEffect(() => { - setIsMobile(window.innerWidth < 768); + const checkMobile = () => setIsMobile(window.innerWidth < 768); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); }, []); const stats = [ @@ -26,77 +29,58 @@ export function DriverProfileMockup() { if (isMobile) { return ( - + - - - 🏎️ - + + 🏎️ + - Driver Profile - Cross-league + Driver Profile + CROSS-LEAGUE - #33 + #33 - + - 2150 GP Rating + 2150 GP RATING - Career Stats + Career Stats {stats.slice(0, 3).map((stat) => ( {stat.value}{stat.suffix} - {stat.label} + {stat.label} ))} - - - Recent Form - - {formData.slice(-6).map((value, i) => ( - - ))} - - ); @@ -120,7 +104,7 @@ export function DriverProfileMockup() { }; return ( - + - - - 🏎️ - + + 🏎️ + - Driver Profile + Driver Profile Cross-league racing identity - #33 + #33 - GridPilot Rating: + GP RATING: - iRating: + iRATING: - + - - 86% - - Career Statistics - Aggregated across all leagues + Career Statistics {stats.map((stat, index) => ( @@ -191,10 +170,10 @@ export function DriverProfileMockup() { key={stat.label} as={motion.div} variants={itemVariants} - bg="bg-iron-gray/50" + bg="panel-gray/40" border - borderColor="border-charcoal-outline" - rounded="lg" + borderColor="border-gray/50" + rounded="none" p={{ base: 1.5, sm: 2, md: 3 }} textAlign="center" > @@ -204,7 +183,7 @@ export function DriverProfileMockup() { delay={index * 0.1} suffix={stat.suffix ?? ''} /> - {stat.label} + {stat.label} ))} @@ -212,22 +191,20 @@ export function DriverProfileMockup() { - Recent Form - Performance trend over last 10 races - - + Recent Form + {formData.map((value, i) => ( ))} - - Last 10 races - Recent - - - - - Teams - Current and past team memberships - - - {[ - { team: 'Red Bull Racing', status: 'Current', color: 'primary-blue' }, - { team: 'Mercedes AMG', status: '2023', color: 'charcoal-outline' } - ].map((team, i) => ( - - - - 🏁 - - - - - - {team.status} - - - - ))} - - - - Active in 3 leagues - ); @@ -318,7 +235,7 @@ function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boo }, [shouldReduceMotion, count, value]); return ( - + {shouldReduceMotion ? value : {rounded}} ); @@ -348,7 +265,7 @@ function AnimatedCounter({ }, [shouldReduceMotion, count, value, delay]); return ( - + {shouldReduceMotion ? value : {rounded}}{suffix} ); diff --git a/apps/website/components/mockups/LeagueDiscoveryMockup.tsx b/apps/website/components/mockups/LeagueDiscoveryMockup.tsx index d47270dbf..b6d350974 100644 --- a/apps/website/components/mockups/LeagueDiscoveryMockup.tsx +++ b/apps/website/components/mockups/LeagueDiscoveryMockup.tsx @@ -43,86 +43,58 @@ export function LeagueDiscoveryMockup() { if (isMobile) { return ( - + - + {['Game', 'Region'].map((filter) => ( - + ))} - + {leagues.map((league) => ( - + {league.icon} - + - - {league.carClass} - - - {league.region} + + {league.carClass} - - - {league.drivers} drivers - - {league.schedule} - - - - - {[...Array(5)].map((_, i) => ( - - {/* eslint-disable-next-line gridpilot-rules/component-classification */} - - - ))} - - - - Join + {league.drivers} DRIVERS • {league.schedule.toUpperCase()} + + JOIN @@ -141,25 +113,25 @@ export function LeagueDiscoveryMockup() { }; const cardVariants = { - hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 20 }, + hidden: { opacity: 0, x: shouldReduceMotion ? 0 : -20 }, visible: { opacity: 1, - y: 0, - transition: { type: 'spring' as const, stiffness: 200, damping: 20 } + x: 0, + transition: { duration: 0.4 } } }; return ( - + - + - + {['Game', 'Region', 'Skill'].map((filter, i) => ( - + ))} @@ -188,7 +160,7 @@ export function LeagueDiscoveryMockup() { initial="hidden" animate="visible" > - + {leagues.map((league, index) => ( !shouldReduceMotion && setHoveredIndex(index)} onHoverEnd={() => setHoveredIndex(null)} whileHover={shouldReduceMotion ? {} : { - scale: 1.02, - y: -4, + x: 4, + borderColor: '#198CFF30', transition: { duration: 0.2 } }} - bg="bg-iron-gray/80" + bg="panel-gray/20" border - borderColor="border-charcoal-outline" - rounded="lg" + borderColor="border-gray/50" + rounded="none" p={{ base: 1.5, sm: 2, md: 3, lg: 4 }} - // eslint-disable-next-line gridpilot-rules/component-classification - className="backdrop-blur-sm" > - - - + + + {league.icon} + - - - + + + {league.carClass} - + {league.region} - - - {league.skill} - - - - - - - {/* eslint-disable-next-line gridpilot-rules/component-classification */} - - - - {league.drivers} drivers - - - - - {/* eslint-disable-next-line gridpilot-rules/component-classification */} - - - - {league.schedule} - - - - - - - {[...Array(5)].map((_, i) => ( - - {/* eslint-disable-next-line gridpilot-rules/component-classification */} - - - ))} + - {league.rating} + {league.drivers} DRIVERS + + + {league.schedule.toUpperCase()} - + - Join - - - - - View + JOIN diff --git a/apps/website/components/mockups/LeagueHomeMockup.tsx b/apps/website/components/mockups/LeagueHomeMockup.tsx index cf15f47d8..e0b11c383 100644 --- a/apps/website/components/mockups/LeagueHomeMockup.tsx +++ b/apps/website/components/mockups/LeagueHomeMockup.tsx @@ -17,47 +17,45 @@ export function LeagueHomeMockup() { if (isMobile) { return ( - + - + 🏆 + - Super GT - Round 8/12 + Super GT + ROUND 8/12 - Next Race - - + Next Race + + 🏁 - - + + - + - Latest Result - - - - - + Latest Result + + + + + - - - + + + @@ -80,7 +78,7 @@ export function LeagueHomeMockup() { }; return ( - + - + 🏆 + - Super GT Championship + Super GT Championship - Season 3 • Round 8/12 + SEASON 3 • ROUND 8/12 - - Your league's dedicated home page - - Upcoming Races - - Calendar automatically synced from iRacing - + Upcoming Races {[1, 2, 3].map((i) => ( - + 🏁 - - + + {i === 1 && ( - + )} @@ -184,28 +155,18 @@ export function LeagueHomeMockup() { - Recent Results - - Results appear instantly after each race - - - - - - + Recent Results + + + + + {[1, 2].map((i) => ( - - - + + + ))} diff --git a/apps/website/components/mockups/MockupStack.tsx b/apps/website/components/mockups/MockupStack.tsx index 7870897b5..83049f4de 100644 --- a/apps/website/components/mockups/MockupStack.tsx +++ b/apps/website/components/mockups/MockupStack.tsx @@ -30,7 +30,7 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) { return (
{ + const getStatusStyles = (status: string) => { switch (status) { - case 'pending': return 'bg-charcoal-outline border-charcoal-outline text-gray-500'; - case 'active': return 'bg-warning-amber/20 border-warning-amber text-warning-amber'; - case 'resolved': return 'bg-performance-green/20 border-performance-green text-performance-green'; - default: return 'bg-charcoal-outline border-charcoal-outline text-gray-500'; + case 'pending': return 'bg-panel-gray border-gray-700 text-gray-600'; + case 'active': return 'bg-warning-amber/10 border-warning-amber text-warning-amber'; + case 'resolved': return 'bg-success-green/10 border-success-green text-success-green'; + default: return 'bg-panel-gray border-gray-700 text-gray-600'; } }; if (isMobile) { return ( - + {steps.map((step, i) => ( @@ -57,41 +54,36 @@ export function ProtestWorkflowMockup() { - {/* eslint-disable-next-line gridpilot-rules/component-classification */} - {step.name} + {step.name} {i < steps.length - 1 && ( - {/* eslint-disable-next-line gridpilot-rules/component-classification */} - + )} ))} - + @@ -100,21 +92,19 @@ export function ProtestWorkflowMockup() { } const stepVariants = { - hidden: { opacity: 0, scale: shouldReduceMotion ? 1 : 0.8 }, + hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 10 }, visible: (i: number) => ({ opacity: 1, - scale: 1, + y: 0, transition: { delay: shouldReduceMotion ? 0 : i * 0.2, - type: 'spring' as const, - stiffness: 200, - damping: 20 + duration: 0.4 } }) }; return ( - + {steps.map((step, i) => ( @@ -134,84 +124,48 @@ export function ProtestWorkflowMockup() { position="relative" w={{ base: 8, sm: 10, md: 12, lg: 14 }} h={{ base: 8, sm: 10, md: 12, lg: 14 }} - rounded="lg" + rounded="none" display="flex" alignItems="center" justifyContent="center" mb={{ base: 1, sm: 1.5, md: 2 }} border - borderWidth="2px" - // eslint-disable-next-line gridpilot-rules/component-classification - className={getStatusColor(step.status)} + borderWidth="1px" + className={getStatusStyles(step.status)} whileHover={shouldReduceMotion ? {} : { - scale: 1.1, - boxShadow: step.status === 'active' - ? '0 0 32px rgba(255,197,86,0.4)' - : step.status === 'resolved' - ? '0 0 32px rgba(111,227,122,0.4)' - : '0 0 20px rgba(34,38,42,0.4)', + scale: 1.05, + borderColor: '#198CFF', transition: { duration: 0.2 } }} - transition={{ type: 'spring', stiffness: 300, damping: 15 }} > - {/* eslint-disable-next-line gridpilot-rules/component-classification */} {step.status === 'active' && ( - + )} {step.name} - {i < steps.length - 1 && ( - {/* eslint-disable-next-line gridpilot-rules/component-classification */} - + )} @@ -225,9 +179,9 @@ export function ProtestWorkflowMockup() { animate={{ opacity: 1, scaleX: 1 }} transition={{ delay: shouldReduceMotion ? 0 : 0.8, duration: 0.6 }} position="relative" - h={{ base: 0.5, md: 1 }} - bg="bg-charcoal-outline" - rounded="full" + h="1" + bg="white/5" + rounded="none" overflow="hidden" > + {/* Race result - clean and simple */} - + - - P3 + + P3 - Watkins Glen - GT3 Sprint + Watkins Glen + GT3 SPRINT @@ -55,30 +55,32 @@ export function RaceHistoryMockup() { {/* Simple arrow */} - + {/* Updates - minimal */} - + Profile Updated - - Stats ↑ + + STATS ↑ - - +12 + + +12 @@ -92,7 +94,7 @@ export function RaceHistoryMockup() { // Desktop version - richer with more updates return ( - + {/* Race Result Card - Enhanced */} - + - - + + - P3 + P3 - 🏁 - Watkins Glen + + Watkins Glen - GT3 Sprint Race + GT3 SPRINT RACE - 24 drivers + 24 DRIVERS - 45 min - - - • - - - Just finished + 45 MIN @@ -178,12 +163,13 @@ export function RaceHistoryMockup() { }} transition={{ duration: 2, repeat: Infinity }} > - + Auto-sync @@ -192,27 +178,28 @@ export function RaceHistoryMockup() { {/* Profile Updates Grid - More detailed */} - - + + Profile Updates {/* Career Stats Update */} - + Career Stats - Wins: 24 → 25 + WINS: 24 → 25 {/* Rating Update */} - + Rating 1342 → 1354 {/* Season Points Update */} - + Season - 248 → 266 pts + 248 → 266 PTS {/* Team Points Update */} - + Team - Contributing + CONTRIBUTING diff --git a/apps/website/components/mockups/SimPlatformMockup.tsx b/apps/website/components/mockups/SimPlatformMockup.tsx index 0ac6b3ccc..908e488a9 100644 --- a/apps/website/components/mockups/SimPlatformMockup.tsx +++ b/apps/website/components/mockups/SimPlatformMockup.tsx @@ -18,30 +18,30 @@ export function SimPlatformMockup() { // Simple mobile version - just the essence of cross-platform if (isMobile) { return ( - + {/* Active Platform - Clean */} - + - - - iR + + iR - iRacing - Active + iRacing + ACTIVE {/* Simple "more coming" indicator */} - - More platforms coming + + More platforms coming @@ -51,150 +51,161 @@ export function SimPlatformMockup() { // Desktop version return ( - + - - Platform Support + + Platform Support - Active: 1 | Planned: 3 + ACTIVE: 1 | PLANNED: 3 {/* iRacing - Active */} - + - - - iR + + iR - iRacing + iRacing - Active + ACTIVE - Full integration + FULL INTEGRATION {/* ACC - Future */} - + - - AC + + AC - ACC + ACC - Planned + PLANNED - Coming later + COMING LATER {/* rFactor 2 - Future */} - + - - rF + + rF - rFactor 2 + rFactor 2 - Planned + PLANNED - Coming later + COMING LATER {/* LMU - Future */} - + - - LM + + LM - Le Mans Ult. + Le Mans Ult. - Planned + PLANNED - Coming later + COMING LATER - + Your identity stays with you across platforms diff --git a/apps/website/components/mockups/StandingsTableMockup.tsx b/apps/website/components/mockups/StandingsTableMockup.tsx index 4f7a45cee..046e9ea72 100644 --- a/apps/website/components/mockups/StandingsTableMockup.tsx +++ b/apps/website/components/mockups/StandingsTableMockup.tsx @@ -17,30 +17,30 @@ export function StandingsTableMockup() { if (isMobile) { return ( - + - + - # + POS - Driver + Driver - Pts + PTS - + {[1, 2, 3, 4, 5].map((i) => ( {i} - - - 🏎️ - - - + - + - + {300 - i * 20} @@ -104,47 +96,48 @@ export function StandingsTableMockup() { } const getRowAnimation = (i: number) => ({ - hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 10 }, + hidden: { opacity: 0, x: shouldReduceMotion ? 0 : -10 }, visible: { opacity: 1, - y: 0, + x: 0, transition: { delay: shouldReduceMotion ? 0 : i * 0.05, - type: 'spring' as const, - stiffness: 300, - damping: 24 + duration: 0.3 } } }); return ( - + Real-time standings updated after every race - + - # + POS Driver @@ -152,9 +145,8 @@ export function StandingsTableMockup() { // eslint-disable-next-line gridpilot-rules/component-classification style={{ fontSize: '10px' }} font="mono" - color="text-gray-400" - // eslint-disable-next-line gridpilot-rules/component-classification - className="hidden md:block" + color="text-gray-500" + className="hidden md:block uppercase tracking-widest" > Wins @@ -163,21 +155,16 @@ export function StandingsTableMockup() { // eslint-disable-next-line gridpilot-rules/component-classification style={{ fontSize: '10px' }} font="mono" - color="text-gray-400" + color="text-gray-500" + className="uppercase tracking-widest" > Points - - ● - - + {[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( !shouldReduceMotion && setHoveredRow(i)} onHoverEnd={() => setHoveredRow(null)} whileHover={shouldReduceMotion ? {} : { - scale: 1.01, - boxShadow: '0 0 20px rgba(25,140,255,0.3)', + x: 4, + borderColor: '#198CFF30', transition: { duration: 0.15 } }} > @@ -208,30 +195,23 @@ export function StandingsTableMockup() { as={motion.div} h={{ base: 6, sm: 7 }} w={{ base: 6, sm: 7 }} - rounded="full" + rounded="none" display="flex" alignItems="center" justifyContent="center" - weight="semibold" + weight="bold" // eslint-disable-next-line gridpilot-rules/component-classification - style={{ fontSize: '12px' }} - bg={i <= 3 ? 'bg-primary-blue' : 'bg-charcoal-outline'} - color={i <= 3 ? 'text-white' : 'text-gray-400'} - animate={ - shouldReduceMotion ? {} : i <= 3 && hoveredRow === i - ? { scale: 1.15, boxShadow: '0 0 28px rgba(25,140,255,0.5)' } - : {} - } + style={{ fontSize: '11px' }} + bg={i <= 3 ? 'primary-accent' : 'panel-gray'} + color={i <= 3 ? 'text-white' : 'text-gray-500'} + font="mono" > {i} - - 🏎️ - - + - @@ -272,13 +252,13 @@ function AnimatedPoints({ const percentage = (points / 300) * 100; return ( - + {shouldReduceMotion ? points : {spring}} diff --git a/apps/website/components/mockups/TeamCompetitionMockup.tsx b/apps/website/components/mockups/TeamCompetitionMockup.tsx index 144d23c28..ea886855e 100644 --- a/apps/website/components/mockups/TeamCompetitionMockup.tsx +++ b/apps/website/components/mockups/TeamCompetitionMockup.tsx @@ -16,15 +16,15 @@ export function TeamCompetitionMockup() { setIsMobile(window.innerWidth < 768); }, []); - const teamColors = ['#198CFF', '#6FE37A', '#FFC556', '#43C9E6', '#9333EA']; + const teamColors = ['#198CFF', '#6FE37A', '#FFBE4D', '#4ED4E0', '#9333EA']; if (isMobile) { return ( - + - Drivers - + Drivers + {[1, 2, 3].map((i) => ( - {i} - - - 🏎️ + {i} - + - + ))} - + - Constructors - + Constructors + {[1, 2, 3].map((i) => ( - - 🏁 - - - + + @@ -160,11 +125,7 @@ export function TeamCompetitionMockup() { visible: { opacity: 1, x: 0, - transition: { - type: 'spring' as const, - stiffness: 100, - damping: 20 - } + transition: { duration: 0.4 } } }; @@ -173,30 +134,24 @@ export function TeamCompetitionMockup() { visible: { opacity: 1, x: 0, - transition: { - type: 'spring' as const, - stiffness: 100, - damping: 20 - } + transition: { duration: 0.4 } } }; const rowVariants = { - hidden: { opacity: 0, scale: shouldReduceMotion ? 1 : 0.95 }, + hidden: { opacity: 0, x: shouldReduceMotion ? 0 : -10 }, visible: (i: number) => ({ opacity: 1, - scale: 1, + x: 0, transition: { delay: shouldReduceMotion ? 0 : 0.3 + i * 0.05, - type: 'spring' as const, - stiffness: 300, - damping: 25 + duration: 0.3 } }) }; return ( - + - - - Drivers - + + DRIVERS - + {[1, 2, 3, 4, 5].map((i) => ( !shouldReduceMotion && setHoveredDriver(i)} onHoverEnd={() => setHoveredDriver(null)} whileHover={shouldReduceMotion ? {} : { - scale: 1.02, - boxShadow: `0 0 20px ${teamColors[i-1]}40`, + x: 4, + borderColor: `${teamColors[i-1]}40`, transition: { duration: 0.15 } }} > @@ -248,65 +196,35 @@ export function TeamCompetitionMockup() { top="0" bottom="0" w="0.5" - // eslint-disable-next-line gridpilot-rules/component-classification style={{ backgroundColor: teamColors[i-1] }} /> - {i} - - - 🏎️ + {i} - + - - {hoveredDriver === i && ( - - )} + ))} - - - - - Constructors - + + CONSTRUCTORS - + {[1, 2, 3, 4, 5].map((i) => ( !shouldReduceMotion && setHoveredTeam(i)} onHoverEnd={() => setHoveredTeam(null)} whileHover={shouldReduceMotion ? {} : { - scale: 1.02, - boxShadow: `0 0 20px ${teamColors[i-1]}40`, + x: -4, + borderColor: `${teamColors[i-1]}40`, transition: { duration: 0.15 } }} > - - 🏁 - - - + + - {i === 3 && ( - - - = - - - )} - {hoveredTeam === i && ( - - )} ))} diff --git a/apps/website/components/mockups/WorkflowMockup.tsx b/apps/website/components/mockups/WorkflowMockup.tsx index 7224af897..756864e08 100644 --- a/apps/website/components/mockups/WorkflowMockup.tsx +++ b/apps/website/components/mockups/WorkflowMockup.tsx @@ -1,4 +1,4 @@ - +'use client'; import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; import { CheckCircle2, LucideIcon } from 'lucide-react'; @@ -43,16 +43,16 @@ export function WorkflowMockup({ steps }: WorkflowMockupProps) { if (!isMounted) { return ( - + {steps.map((step) => ( - + - {step.title} + {step.title} ))} @@ -63,15 +63,15 @@ export function WorkflowMockup({ steps }: WorkflowMockupProps) { return ( - + {/* Connection Lines */} - + + {isActive && ( + + )} {isCompleted ? ( - + ) : ( - + )} {step.title} @@ -153,15 +157,15 @@ export function WorkflowMockup({ steps }: WorkflowMockupProps) { mt={4} pt={4} borderTop={true} - borderColor="border-charcoal-outline" + borderColor="border-gray/30" display={{ base: 'block', sm: 'none' }} > - - Step {activeStep + 1}: {steps[activeStep]?.title || ''} + + STEP {activeStep + 1}: {steps[activeStep]?.title || ''} - - {steps[activeStep]?.description || ''} + + {steps[activeStep]?.description.toUpperCase() || ''} diff --git a/apps/website/components/notifications/ModalNotification.tsx b/apps/website/components/notifications/ModalNotification.tsx index 5746fc3f4..9f58b552d 100644 --- a/apps/website/components/notifications/ModalNotification.tsx +++ b/apps/website/components/notifications/ModalNotification.tsx @@ -44,40 +44,40 @@ const notificationIcons: Record = { const notificationColors: Record = { protest_filed: { - bg: 'bg-red-500/10', - border: 'border-red-500/50', - text: 'text-red-400', - glow: 'shadow-[0_0_60px_rgba(239,68,68,0.3)]', + bg: 'bg-critical-red/10', + border: 'border-critical-red/50', + text: 'text-critical-red', + glow: 'shadow-[0_0_60px_rgba(227,92,92,0.3)]', }, protest_defense_requested: { bg: 'bg-warning-amber/10', border: 'border-warning-amber/50', text: 'text-warning-amber', - glow: 'shadow-[0_0_60px_rgba(245,158,11,0.3)]', + glow: 'shadow-[0_0_60px_rgba(255,197,86,0.3)]', }, protest_vote_required: { - bg: 'bg-primary-blue/10', - border: 'border-primary-blue/50', - text: 'text-primary-blue', + bg: 'bg-primary-accent/10', + border: 'border-primary-accent/50', + text: 'text-primary-accent', glow: 'shadow-[0_0_60px_rgba(25,140,255,0.3)]', }, penalty_issued: { - bg: 'bg-red-500/10', - border: 'border-red-500/50', - text: 'text-red-400', - glow: 'shadow-[0_0_60px_rgba(239,68,68,0.3)]', + bg: 'bg-critical-red/10', + border: 'border-critical-red/50', + text: 'text-critical-red', + glow: 'shadow-[0_0_60px_rgba(227,92,92,0.3)]', }, race_performance_summary: { - bg: 'bg-gradient-to-br from-yellow-400/20 via-orange-500/20 to-red-500/20', - border: 'border-yellow-400/60', - text: 'text-yellow-400', - glow: 'shadow-[0_0_80px_rgba(251,191,36,0.4)]', + bg: 'bg-panel-gray', + border: 'border-warning-amber/60', + text: 'text-warning-amber', + glow: 'shadow-[0_0_80px_rgba(255,197,86,0.2)]', }, race_final_results: { - bg: 'bg-gradient-to-br from-purple-500/20 via-pink-500/20 to-indigo-500/20', - border: 'border-purple-400/60', - text: 'text-purple-400', - glow: 'shadow-[0_0_80px_rgba(168,85,247,0.4)]', + bg: 'bg-panel-gray', + border: 'border-primary-accent/60', + text: 'text-primary-accent', + glow: 'shadow-[0_0_80px_rgba(25,140,255,0.2)]', }, }; @@ -123,10 +123,10 @@ export function ModalNotification({ const NotificationIcon = notificationIcons[notification.type] || AlertCircle; const colors = notificationColors[notification.type] || { - bg: 'bg-warning-amber/10', - border: 'border-warning-amber/50', - text: 'text-warning-amber', - glow: 'shadow-[0_0_60px_rgba(245,158,11,0.3)]', + bg: 'bg-panel-gray', + border: 'border-border-gray', + text: 'text-gray-400', + glow: 'shadow-card', }; const data: Record = notification.data ?? {}; @@ -160,7 +160,6 @@ export function ModalNotification({ // Special celebratory styling for race notifications const isRaceNotification = notification.type.startsWith('race_'); - const isPerformanceSummary = notification.type === 'race_performance_summary'; const provisionalRatingChange = getNumber(data.provisionalRatingChange) ?? 0; const finalRatingChange = getNumber(data.finalRatingChange) ?? 0; @@ -177,10 +176,8 @@ export function ModalNotification({ justifyContent="center" p={4} transition - bg={isVisible ? 'bg-black/70' : 'bg-transparent'} - // eslint-disable-next-line gridpilot-rules/component-classification + bg={isVisible ? 'bg-black/80' : 'bg-transparent'} className={isVisible ? 'backdrop-blur-sm' : ''} - hoverBg={isRaceNotification ? 'bg-gradient-to-br from-black/80 via-indigo-900/10 to-black/80' : undefined} > - {/* Header with pulse animation */} + {/* Header */} - + - {isRaceNotification ? (isPerformanceSummary ? '🏁 Race Complete!' : '🏆 Championship Update') : 'Action Required'} + {isRaceNotification ? 'Race Update' : 'Action Required'} - + {notification.title} @@ -249,7 +236,7 @@ export function ModalNotification({ onClick={() => onDismiss(notification)} variant="ghost" size="md" - color="text-gray-400" + color="text-gray-500" title="Dismiss notification" /> )} @@ -257,17 +244,11 @@ export function ModalNotification({ {/* Body */} - + {notification.message} @@ -275,16 +256,16 @@ export function ModalNotification({ {/* Race performance stats */} {isRaceNotification && ( - - - POSITION + + + POSITION {notification.data?.position === 'DNF' ? 'DNF' : `P${notification.data?.position || '?'}`} - - RATING CHANGE - = 0 ? 'text-green-400' : 'text-red-400'} block> + + RATING + = 0 ? 'text-success-green' : 'text-critical-red'} block> {ratingChange >= 0 ? '+' : ''} {ratingChange} @@ -294,12 +275,12 @@ export function ModalNotification({ {/* Deadline warning */} {hasDeadline && !isRaceNotification && ( - + - Response Required - - Please respond by {deadline ? deadline.toLocaleDateString() : ''} at {deadline ? deadline.toLocaleTimeString() : ''} + Response Required + + By {deadline ? deadline.toLocaleDateString() : ''} {deadline ? deadline.toLocaleTimeString() : ''} @@ -307,9 +288,9 @@ export function ModalNotification({ {/* Additional context from data */} {protestId && ( - - Related Protest - + + PROTEST ID + {protestId} @@ -321,10 +302,8 @@ export function ModalNotification({ px={6} py={4} borderTop - borderColor={isRaceNotification ? (isPerformanceSummary ? 'border-yellow-400/60' : 'border-purple-400/60') : 'border-charcoal-outline'} - bg={isRaceNotification ? undefined : 'bg-iron-gray/30'} - // eslint-disable-next-line gridpilot-rules/component-classification - className={isRaceNotification ? (isPerformanceSummary ? 'bg-gradient-to-r from-yellow-500/10 to-orange-500/10' : 'bg-gradient-to-r from-purple-500/10 to-pink-500/10') : ''} + borderColor="border-border-gray" + bg="graphite-black" > {notification.actions && notification.actions.length > 0 ? ( @@ -333,9 +312,7 @@ export function ModalNotification({ key={index} variant={action.type === 'primary' ? 'primary' : 'secondary'} onClick={() => handleAction(action)} - bg={action.type === 'danger' ? 'bg-red-500' : undefined} - color={action.type === 'danger' ? 'text-white' : undefined} - shadow={isRaceNotification ? 'lg' : undefined} + className={action.type === 'danger' ? 'bg-critical-red hover:bg-critical-red/90' : ''} > {action.label} @@ -346,22 +323,14 @@ export function ModalNotification({ - ) : ( @@ -375,9 +344,9 @@ export function ModalNotification({ {/* Cannot dismiss warning */} {notification.requiresResponse && !isRaceNotification && ( - - - ⚠️ This notification requires your action and cannot be dismissed + + + This action is required to continue )} diff --git a/apps/website/tailwind.config.js b/apps/website/tailwind.config.js index d2064aeb8..867f437bf 100644 --- a/apps/website/tailwind.config.js +++ b/apps/website/tailwind.config.js @@ -10,41 +10,39 @@ module.exports = { ], theme: { extend: { + backgroundImage: { + 'radial-gradient': 'radial-gradient(var(--tw-gradient-stops))', + }, colors: { - 'deep-graphite': '#0E0F11', - 'iron-gray': '#181B1F', - 'charcoal-outline': '#22262A', + 'graphite-black': '#0C0D0F', + 'panel-gray': '#141619', + 'border-gray': '#23272B', + 'primary-accent': '#198CFF', + 'telemetry-aqua': '#4ED4E0', + 'warning-amber': '#FFBE4D', + 'success-green': '#6FE37A', + 'critical-red': '#E35C5C', + // Legacy mappings for compatibility during transition + 'deep-graphite': '#0C0D0F', + 'iron-gray': '#141619', + 'charcoal-outline': '#23272B', 'primary-blue': '#198CFF', 'performance-green': '#6FE37A', - 'warning-amber': '#FFC556', - 'neon-aqua': '#43C9E6', - 'racing-red': '#E31E24', - 'carbon-black': '#0A0A0A', - 'metallic-silver': '#C0C0C8', + 'racing-red': '#E35C5C', }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], }, boxShadow: { - 'glow': '0 0 20px rgba(25, 140, 255, 0.3)', - 'glow-strong': '0 0 28px rgba(25, 140, 255, 0.5)', - 'card': '0 8px 24px rgba(0, 0, 0, 0.12)', - 'racing': '0 4px 16px rgba(227, 30, 36, 0.15)', + 'card': '0 4px 12px rgba(0, 0, 0, 0.2)', + }, + transitionDuration: { + 'smooth': '150ms', }, transitionTimingFunction: { - 'spring': 'cubic-bezier(0.34, 1.56, 0.64, 1)', - 'speed': 'cubic-bezier(0.22, 1, 0.36, 1)', - }, - animation: { - 'speed-pulse': 'speed-pulse 2s ease-in-out infinite', - }, - keyframes: { - 'speed-pulse': { - '0%, 100%': { opacity: '0.5' }, - '50%': { opacity: '1' }, - }, + 'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)', }, }, }, plugins: [], -} \ No newline at end of file +} diff --git a/apps/website/templates/HomeTemplate.tsx b/apps/website/templates/HomeTemplate.tsx index c9e27f7c6..804db90ee 100644 --- a/apps/website/templates/HomeTemplate.tsx +++ b/apps/website/templates/HomeTemplate.tsx @@ -4,27 +4,20 @@ import { AlternatingSection } from '@/components/landing/AlternatingSection'; import { FAQ } from '@/components/landing/FAQ'; import { FeatureGrid } from '@/components/landing/FeatureGrid'; import { LandingHero } from '@/components/landing/LandingHero'; -import { FeatureItem, ResultItem, StepItem } from '@/components/landing/LandingItems'; +import { DiscoverySection } from '@/components/landing/DiscoverySection'; +import { FeatureItem, ResultItem, StepItem } from '@/ui/LandingItems'; import { CareerProgressionMockup } from '@/components/mockups/CareerProgressionMockup'; import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup'; import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup'; import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup'; import { ModeGuard } from '@/components/shared/ModeGuard'; -import { routes } from '@/lib/routing/RouteConfig'; -import { getMediaUrl } from '@/lib/utilities/media'; import { Box } from '@/ui/Box'; -import { Button } from '@/ui/Button'; -import { Card } from '@/ui/Card'; -import { Container } from '@/ui/Container'; import { DiscordCTA } from '@/ui/DiscordCTA'; import { Footer } from '@/ui/Footer'; -import { Grid } from '@/ui/Grid'; -import { Heading } from '@/ui/Heading'; -import { Image } from '@/ui/Image'; -import { Link } from '@/ui/Link'; import { Stack } from '@/ui/Stack'; -import { Surface } from '@/ui/Surface'; import { Text } from '@/ui/Text'; +import { TelemetryLine } from '@/ui/TelemetryLine'; +import { Glow } from '@/ui/Glow'; export interface HomeViewData { isAlpha: boolean; @@ -53,220 +46,132 @@ interface HomeTemplateProps { export function HomeTemplate({ viewData }: HomeTemplateProps) { return ( - + + + + + {/* Section 1: A Persistent Identity */} - - - Your races, your seasons, your progress — finally in one place. - - - - - + + + + + Your races, your seasons, your progress — finally in one place. + + + + + + + + + iRacing gives you physics. GridPilot gives you a career. + + - - iRacing gives you physics. GridPilot gives you a career. - - - } - mockup={} - layout="text-left" - /> + } + mockup={} + layout="text-left" + /> + {/* Section 2: Results That Actually Stay */} - - - Every race you run stays with you. - - - - - + + + + + Every race you run stays with you. + + + + + + + + + Your racing career, finally in one place. + + - - Your racing career, finally in one place. - - - } - mockup={} - layout="text-right" - /> + } + mockup={} + layout="text-right" + /> + + + {/* Section 3: Automatic Session Creation */} - - - Setting up league races used to mean clicking through iRacing's wizard 20 times. - - - - - + + + + + Setting up league races used to mean clicking through iRacing's wizard 20 times. + + + + + + + + + Automation instead of repetition. + + - - Automation instead of repetition. - - - } - mockup={} - layout="text-left" - /> + } + mockup={} + layout="text-left" + /> + {/* Section 4: Game-Agnostic Platform */} - - - Right now, we're focused on making iRacing league racing better. - - - But sims come and go. Your leagues, your teams, your rating — those stay. - - - GridPilot is built to outlast any single platform. - - - When the next sim arrives, your competitive identity moves with you. - - - } - mockup={} - layout="text-right" - /> + + + + + Right now, we're focused on making iRacing league racing better. + + + But sims come and go. Your leagues, your teams, your rating — those stay. + + + + GridPilot is built to outlast any single platform. When the next sim arrives, your competitive identity moves with you. + + + + } + mockup={} + layout="text-right" + /> + {/* Alpha-only discovery section */} - - - - Discover the grid - - Explore leagues, teams, and races that make up the GridPilot ecosystem. - - - - - {/* Top leagues */} - - - - Featured leagues - - - - - - {viewData.topLeagues.slice(0, 4).map((league) => ( - - - - - {league.name.split(' ').map((word) => word[0]).join('').slice(0, 3).toUpperCase()} - - - - {league.name} - {league.description} - - - - ))} - - - - - {/* Teams */} - - - - Teams on the grid - - - - - - {viewData.teams.slice(0, 4).map(team => ( - - - - {team.name} - - - {team.name} - {team.description} - - - - ))} - - - - - {/* Upcoming races */} - - - - Upcoming races - - - - - {viewData.upcomingRaces.length === 0 ? ( - - No races scheduled in this demo snapshot. - - ) : ( - - {viewData.upcomingRaces.map(race => ( - - - - {race.track} - {race.car} - - - {race.formattedDate} - - - - ))} - - )} - - - - - + + + + diff --git a/apps/website/ui/Badge.tsx b/apps/website/ui/Badge.tsx index e6ad84aff..165191b86 100644 --- a/apps/website/ui/Badge.tsx +++ b/apps/website/ui/Badge.tsx @@ -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 = [ diff --git a/apps/website/ui/Button.tsx b/apps/website/ui/Button.tsx index 5d9dcacb5..34d255443 100644 --- a/apps/website/ui/Button.tsx +++ b/apps/website/ui/Button.tsx @@ -6,7 +6,7 @@ interface ButtonProps extends Omit, 'as' children: ReactNode; onClick?: MouseEventHandler; 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(({ 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 = [ diff --git a/apps/website/ui/Card.tsx b/apps/website/ui/Card.tsx index 47abfa97a..978a66681 100644 --- a/apps/website/ui/Card.tsx +++ b/apps/website/ui/Card.tsx @@ -13,7 +13,7 @@ interface CardProps extends Omit, 'children' | 'className' | 'on children: ReactNode; className?: string; onClick?: MouseEventHandler; - 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({ {children} diff --git a/apps/website/ui/DecorativeBlur.tsx b/apps/website/ui/DecorativeBlur.tsx index 06c3cecd4..5ef255a54 100644 --- a/apps/website/ui/DecorativeBlur.tsx +++ b/apps/website/ui/DecorativeBlur.tsx @@ -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 = { diff --git a/apps/website/ui/DiscordCTA.tsx b/apps/website/ui/DiscordCTA.tsx index 6205df69e..65571cab9 100644 --- a/apps/website/ui/DiscordCTA.tsx +++ b/apps/website/ui/DiscordCTA.tsx @@ -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 ( - - + + + {/* Discord brand accent */} - + {/* Header */} - + - + + + - - - Join us on Discord - + + + Join the Grid on Discord + {/* Personal message */} - - - - GridPilot is a solo developer project, and I'm building it because I got tired of the chaos in league racing. + + + + GridPilot is a solo developer project built for the community. - - This is early days, and I need your help. Join the Discord to: + + We are in early alpha. Join us to help shape the future of motorsport infrastructure. Your feedback directly influences the roadmap. {/* Benefits grid */} - + {/* CTA Button */} - + - - 💡 Get a link to our early alpha view in the Discord - - - {!process.env.NEXT_PUBLIC_DISCORD_URL && ( - - Note: Configure NEXT_PUBLIC_DISCORD_URL in your environment variables + + + Early Alpha Access Available - )} + - - {/* Footer note */} - - - This is a community effort. Every voice matters. Let's build something that actually works for league racing. - - - - + + ); } @@ -159,30 +153,29 @@ function BenefitItem({ icon, title, description }: { icon: LucideIcon, title: st - + - - {title} - {description} + + {title} + {description} ); diff --git a/apps/website/ui/Footer.tsx b/apps/website/ui/Footer.tsx index c067d1a13..4c790c485 100644 --- a/apps/website/ui/Footer.tsx +++ b/apps/website/ui/Footer.tsx @@ -8,41 +8,31 @@ const xUrl = process.env.NEXT_PUBLIC_X_URL || '#'; export function Footer() { return ( - - + + - + {/* Racing stripe accent */} - - - + + + {/* Personal message */} - - GridPilot - - + 🏁 Built by a sim racer, for sim racers - + Just a fellow racer tired of spreadsheets and chaos. GridPilot is my passion project to make league racing actually fun again. @@ -51,50 +41,39 @@ export function Footer() { - 💬 Join Discord + 💬 Discord - 𝕏 Follow on X + 𝕏 Twitter {/* Development status */} - + ⚡ Early development • Feedback welcome - - Questions? Find me on Discord + + © {new Date().getFullYear()} GridPilot diff --git a/apps/website/ui/Glow.tsx b/apps/website/ui/Glow.tsx new file mode 100644 index 000000000..6d77858f1 --- /dev/null +++ b/apps/website/ui/Glow.tsx @@ -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 ( + + ); +} diff --git a/apps/website/ui/Header.tsx b/apps/website/ui/Header.tsx index 592ec9d32..9fa4062f9 100644 --- a/apps/website/ui/Header.tsx +++ b/apps/website/ui/Header.tsx @@ -7,7 +7,7 @@ interface HeaderProps { export function Header({ children }: HeaderProps) { return ( -
+
{children} diff --git a/apps/website/ui/Heading.tsx b/apps/website/ui/Heading.tsx index 8bb019572..04672112e 100644 --- a/apps/website/ui/Heading.tsx +++ b/apps/website/ui/Heading.tsx @@ -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 = { diff --git a/apps/website/ui/Input.tsx b/apps/website/ui/Input.tsx index 3767598be..03cfd53a4 100644 --- a/apps/website/ui/Input.tsx +++ b/apps/website/ui/Input.tsx @@ -12,15 +12,15 @@ interface InputProps extends InputHTMLAttributes { export const Input = forwardRef( ({ 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 ( {label && ( - + {label} )} @@ -34,13 +34,14 @@ export const Input = forwardRef( zIndex={10} display="flex" center + className="text-gray-500" > {icon} )} {errorMessage && ( - + {errorMessage} )} @@ -50,4 +51,4 @@ export const Input = forwardRef( } ); -Input.displayName = 'Input'; \ No newline at end of file +Input.displayName = 'Input'; diff --git a/apps/website/ui/LandingItems.tsx b/apps/website/ui/LandingItems.tsx new file mode 100644 index 000000000..e36ec09a0 --- /dev/null +++ b/apps/website/ui/LandingItems.tsx @@ -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 ( + + + + + {text} + + + + ); +} + +export function ResultItem({ text, color }: { text: string, color: string }) { + return ( + + + + + {text} + + + + ); +} + +export function StepItem({ step, text }: { step: number, text: string }) { + return ( + + + + {step.toString().padStart(2, '0')} + + + {text} + + + + ); +} diff --git a/apps/website/ui/LeagueCard.tsx b/apps/website/ui/LeagueCard.tsx index a92708ba2..abc351f7a 100644 --- a/apps/website/ui/LeagueCard.tsx +++ b/apps/website/ui/LeagueCard.tsx @@ -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({ {/* Cover Image */} @@ -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 */} - + {/* Badges - Top Left */} @@ -93,7 +94,7 @@ export function LeagueCard({ {/* Logo */} - + {logoUrl ? ( {/* Title & Description */} - - {name} - - + + + + {name} + + + {description || 'No description available'} {/* Stats Row */} - + {/* Primary Slots (Drivers/Teams/Nations) */} - - {slotLabel} - + + {slotLabel} + {usedSlots}/{maxSlots || '∞'} - + = 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 && ( - - - - {openSlotsCount} open + + + + {openSlotsCount} OPEN )} - {/* Driver count for team leagues */} - {isTeamLeague && ( - - - - {usedDriverSlots ?? 0}/{maxDrivers ?? '∞'} drivers - - - )} - {/* Spacer to push footer to bottom */} {/* Footer Info */} - + {timingSummary && ( - + - + {timingSummary.split('•')[1]?.trim() || timingSummary} @@ -186,8 +180,8 @@ export function LeagueCard({ {/* View Arrow */} - - View + + VIEW diff --git a/apps/website/ui/Link.tsx b/apps/website/ui/Link.tsx index f22a0febd..78556e035 100644 --- a/apps/website/ui/Link.tsx +++ b/apps/website/ui/Link.tsx @@ -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' }; diff --git a/apps/website/ui/MainContent.tsx b/apps/website/ui/MainContent.tsx index a05a9aecd..2e9961784 100644 --- a/apps/website/ui/MainContent.tsx +++ b/apps/website/ui/MainContent.tsx @@ -5,5 +5,5 @@ interface MainContentProps { } export function MainContent({ children }: MainContentProps) { - return
{children}
; + return
{children}
; } \ No newline at end of file diff --git a/apps/website/ui/Section.tsx b/apps/website/ui/Section.tsx index 101e09c2e..06cc239c1 100644 --- a/apps/website/ui/Section.tsx +++ b/apps/website/ui/Section.tsx @@ -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 = [ diff --git a/apps/website/ui/Surface.tsx b/apps/website/ui/Surface.tsx index 48c586e22..ee3df086b 100644 --- a/apps/website/ui/Surface.tsx +++ b/apps/website/ui/Surface.tsx @@ -17,7 +17,7 @@ export function Surface({ as, children, variant = 'default', - rounded = 'lg', + rounded = 'none', border = false, padding = 0, className = '', @@ -26,16 +26,16 @@ export function Surface({ ...props }: SurfaceProps & ComponentPropsWithoutRef) { const variantClasses: Record = { - 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 = { @@ -72,7 +72,7 @@ export function Surface({ const classes = [ variantClasses[variant], roundedClasses[rounded], - border ? 'border border-charcoal-outline' : '', + border ? 'border border-border-gray' : '', paddingClasses[padding] || 'p-0', shadowClasses[shadow], display ? display : '', diff --git a/apps/website/ui/Table.tsx b/apps/website/ui/Table.tsx index 9a594ad09..a508cec8e 100644 --- a/apps/website/ui/Table.tsx +++ b/apps/website/ui/Table.tsx @@ -8,8 +8,8 @@ interface TableProps extends HTMLAttributes { export function Table({ children, className = '', ...props }: TableProps) { return ( - - + +
{children}
@@ -22,7 +22,7 @@ interface TableHeadProps extends HTMLAttributes { export function TableHead({ children, ...props }: TableHeadProps) { return ( - + {children} ); @@ -34,7 +34,7 @@ interface TableBodyProps extends HTMLAttributes { export function TableBody({ children, ...props }: TableBodyProps) { return ( - + {children} ); @@ -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 ( diff --git a/apps/website/ui/TeamCard.tsx b/apps/website/ui/TeamCard.tsx index e2c515839..2899288e8 100644 --- a/apps/website/ui/TeamCard.tsx +++ b/apps/website/ui/TeamCard.tsx @@ -47,53 +47,55 @@ export function TeamCard({ onClick, }: TeamCardProps) { return ( - - + + {/* Header with Logo */} - + {/* Logo */} {logo ? ( {name} ) : ( - + )} + {/* Title & Badges */} - + {name} {isRecruiting && ( - - Recruiting + + RECRUITING )} {/* Performance Level & Category */} - + {performanceBadge} {specializationContent} {categoryBadge} @@ -103,48 +105,43 @@ export function TeamCard({ {/* Content */} - + {/* Description */} {description || 'No description available'} {/* Region & Languages */} {(region || languagesContent) && ( - + {region && ( - - {region} + + {region} )} {languagesContent} )} - {/* Stats Grid */} - {statsContent && ( - - {statsContent} - - )} - {/* Spacer */} @@ -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" > - - - {memberCount} {memberCount === 1 ? 'member' : 'members'} + + + {memberCount} {memberCount === 1 ? 'MEMBER' : 'MEMBERS'} - - View - + + VIEW + diff --git a/apps/website/ui/TelemetryLine.tsx b/apps/website/ui/TelemetryLine.tsx new file mode 100644 index 000000000..4a5d7e7f7 --- /dev/null +++ b/apps/website/ui/TelemetryLine.tsx @@ -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 ( + + ); +} diff --git a/apps/website/ui/UpcomingRaceItem.tsx b/apps/website/ui/UpcomingRaceItem.tsx index a947dcd37..2021c2f17 100644 --- a/apps/website/ui/UpcomingRaceItem.tsx +++ b/apps/website/ui/UpcomingRaceItem.tsx @@ -24,30 +24,35 @@ export function UpcomingRaceItem({ return ( - - {track} - - - {car} - - - - {formattedDate} - - - • - - - {formattedTime} - + + + + + {track} + + + {car} + + + + + {formattedDate} + + + {formattedTime} + + {isMyLeague && ( - - - Your League + + + YOUR LEAGUE )} diff --git a/docs/THEME.md b/docs/THEME.md index a14b2a773..56ffeb359 100644 --- a/docs/THEME.md +++ b/docs/THEME.md @@ -1,141 +1,124 @@ -# GridPilot Theme — “Motorsport Infrastructure, Smoothly Engineered” +🎨 GridPilot Dev Theme — “Precision Racing Minimal” -*A precise, professional motorsport interface with premium smoothness — engineered for trust, control, and long-term use.* +The core idea: +A UI that feels like a race engineer’s dashboard — not a game launcher, not corporate SaaS. ---- +⸻ -## 1. Design Philosophy +1. Identity & Mood -GridPilot should feel like: -- **race control software**, not a game UI -- **infrastructure**, not a startup product -- **engineered**, not styled -- **stable and authoritative**, yet pleasant to use -- **built for years**, not trends +The theme should communicate: + • Precision (like telemetry screens) + • Calm intensity (the feeling before a qualifying lap) + • Authority without ego (used by serious racers) + • Modern minimalism (dev-friendly, clean structure) + • Soft gaming hints (subtle neon touches, not RGB vomit) -The goal is not excitement. -The goal is **confidence**. +It appeals equally to: + • Devs (clean, modular, readable components) + • Gamers (immersive, familiar atmosphere) + • Simracers (motorsport seriousness) -Think: -**“FIA race control x timing screens x modern tooling — with smooth interaction.”** +⸻ ---- +2. Visual Language -## 2. Visual Style +Primary Look + • Dark, matte surfaces + • Thin, crisp separators + • Soft glow accents in blue / cyan / purple + • Avoid aggressive neon or gamer-overload + • Subtle gradient tints (barely visible) -### Core Aesthetic: -- dark, neutral background -- minimal gradients (almost invisible) -- restrained highlights only for meaning -- strict hierarchy -- dense, readable layouts -- smooth transitions only where state changes +Color System + • Base: Near-black graphite (#0C0D0F) + • Surface: Deep charcoal (#141619) + • Outline: Soft steel grey (#23272B) + • Primary Accent: Electric blue (#198CFF) + • Telemetry Accent: Aqua (#4ED4E0) + • Warning: Motorsport amber (#FFBE4D) -Everything should look: -**intentional, measured, and calm.** +Everything should feel instrument-grade, not decorative. ---- +⸻ -### Color Palette (refined) +3. Component Philosophy -- **Graphite Black:** `#0E0F11` -- **Panel Gray:** `#171A1E` -- **Border Gray:** `#22262A` -- **Primary Accent:** `#198CFF` (used sparingly) -- **Success Green:** `#6FE37A` -- **Warning Amber:** `#FFC556` -- **Critical Red:** `#E35C5C` +Cards + • Functional over flashy + • Slight bevel or inset shadow (hint of cockpit panels) + • Dense info, clean hierarchy -No neon. -No playful colors. -Color = meaning, not decoration. +Tables + • High-density + • Thin row dividers + • Highlight on hover only + • Telemetry-coded status colors ---- +Buttons + • Flat by default + • Glow + gradient only on hover + • Snappy micro-animation (motorsport feedback) -## 3. Motion & Interaction +Modals + • Soft frosted blur + • Fast open/close animation + • Dimmed pit-lane-lighting vibe -### Animation Philosophy -Motion exists only to: -- confirm an action -- show hierarchy -- indicate state change +⸻ -Never to impress. +4. Motion & Feedback -### Characteristics: -- short durations (120–200ms) -- low amplitude -- no exaggerated easing -- no elastic bounces -- no decorative movement +Racing-inspired, but minimal: + • Short accelerations (fast ease-out) + • No bounces — this isn’t a game store + • Hover = slight lift + color pulse + • Loading = progress line (like pit limiter light) + • Switching tabs = sliding underline (chicane motion) -**Motion should feel like a well-damped suspension — not a show car.** +Devs should feel the UI is responsive, not playful. ---- +⸻ -## 4. Where Motion Is Allowed +5. Layout Structure -- button press feedback -- panel open / close -- table row expansion -- state changes (pending → approved → completed) -- subtle loading indicators +Think “Telemetry Workspace”: + • Sidebar = dashboard rail + • Header = control bar + • Content = track map / data table zone + • Right panel = session or context info -If motion does not improve clarity → remove it. +Everything modular, easily replaceable, easy to reason about. ---- +⸻ -## 5. What We Explicitly Avoid +6. Tone for Dev-Facing Text + • Short + • Neutral + • Calm + • Technical + • Real-world language (not corporate slang) -- hero animations -- animated backgrounds -- glowing UI chrome -- playful hover gimmicks -- “app store” aesthetics -- anything that reduces trust +Examples: + • “Add race” + • “Review protest” + • “Session updated” + • “Export standings” + • “Sponsor payout queued” -GridPilot must feel: -**reliable before it feels beautiful.** +No hype. No marketing tone inside the app. ---- +⸻ -## 6. Components +7. Emotional Target -### Tables -- primary UI element -- dense, readable -- fixed column logic -- no playful effects +Users should feel: + • In control + • Supported + • Efficient + • Connected to motorsport culture + • Trusting (nothing looks cheap or gimmicky) -### Cards -- functional grouping -- no visual dominance -- secondary to tables +It’s basically: -### Modals -- simple -- fast -- decisive - ---- - -## 7. Typography - -- neutral sans-serif -- excellent numeric readability -- no personality fonts - -Primary goal: -**information clarity, not brand expression.** - ---- - -## 8. Design Principle Summary - -GridPilot is: -- not gamer UI -- not esports branding -- not corporate SaaS - -It is: -**modern motorsport infrastructure software.** \ No newline at end of file +“A premium cockpit dashboard… built by people who race and code.” \ No newline at end of file