From 6df1b50536eaa0a4cccee6a8676dfef044528538 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 20 Jan 2026 00:10:30 +0100 Subject: [PATCH] website refactor --- apps/website/app/globals.css | 8 +- .../website/components/admin/AdminToolbar.tsx | 1 - apps/website/components/layout/AppFooter.tsx | 96 ++++--- apps/website/components/layout/AppHeader.tsx | 157 +++++++---- .../website/components/layout/AppShellBar.tsx | 34 +++ apps/website/components/layout/AppSidebar.tsx | 82 +++--- .../components/layout/HeaderActions.tsx | 30 ++- apps/website/components/layout/PublicNav.tsx | 11 +- .../templates/layout/RootAppShellTemplate.tsx | 32 +-- apps/website/tsc_output.txt | 2 + apps/website/ui/BrandMark.tsx | 31 +-- apps/website/ui/Layout.tsx | 77 +++--- apps/website/ui/NavLink.tsx | 51 +++- docs/THEME.md | 250 ++++++++++-------- 14 files changed, 511 insertions(+), 351 deletions(-) create mode 100644 apps/website/components/layout/AppShellBar.tsx create mode 100644 apps/website/tsc_output.txt diff --git a/apps/website/app/globals.css b/apps/website/app/globals.css index 77f60b44b..a754b0253 100644 --- a/apps/website/app/globals.css +++ b/apps/website/app/globals.css @@ -141,9 +141,9 @@ @layer utilities { /* Precision Racing Utilities */ .glass-panel { - background: rgba(20, 22, 25, 0.7); - backdrop-filter: blur(12px); - border: 1px solid var(--color-outline); + background: rgba(20, 22, 25, 0.85); + backdrop-filter: blur(16px); + border-bottom: 1px solid var(--color-outline); } .subtle-gradient { @@ -161,7 +161,7 @@ left: 0; width: 100%; height: 1px; - background: linear-gradient(90deg, var(--color-primary) 0%, transparent 100%); + background: linear-gradient(90deg, transparent 0%, var(--color-primary) 50%, transparent 100%); opacity: 0.5; } diff --git a/apps/website/components/admin/AdminToolbar.tsx b/apps/website/components/admin/AdminToolbar.tsx index 34ec81ce6..e7e02a51c 100644 --- a/apps/website/components/admin/AdminToolbar.tsx +++ b/apps/website/components/admin/AdminToolbar.tsx @@ -21,7 +21,6 @@ export function AdminToolbar({ return ( {children} diff --git a/apps/website/components/layout/AppFooter.tsx b/apps/website/components/layout/AppFooter.tsx index 829780307..1575d4c91 100644 --- a/apps/website/components/layout/AppFooter.tsx +++ b/apps/website/components/layout/AppFooter.tsx @@ -1,44 +1,72 @@ 'use client'; -import { Surface } from '@/ui/Surface'; -import { Stack } from '@/ui/Stack'; -import { Text } from '@/ui/Text'; import { Box } from '@/ui/Box'; +import { Text } from '@/ui/Text'; +import { Link } from '@/ui/Link'; +import { AppShellBar } from './AppShellBar'; +import { Activity, Database, Server, Wifi } from 'lucide-react'; export function AppFooter() { - return ( - - - - - © {new Date().getFullYear()} GridPilot - - - - Session ready - - + const currentYear = new Date().getFullYear(); - - - - - System Normal - - + return ( + + {/* Left: System Info */} + + + + + GRIDPILOT OS v2.0 + + + + © {currentYear} + + + + {/* Center: Telemetry Status */} + + + + + + + + 12ms - + + {/* Right: Legal & Tools */} + + Terms + Privacy + Status + + + ); +} + +function StatusIndicator({ icon: Icon, label, status }: { icon: any, label: string, status: 'good' | 'warn' | 'bad' }) { + const color = status === 'good' ? 'text-success-green' : status === 'warn' ? 'text-warning-amber' : 'text-critical-red'; + + return ( + + + + {label} + + + ); +} + +function FooterLink({ href, children }: { href: string, children: React.ReactNode }) { + return ( + + {children} + ); } diff --git a/apps/website/components/layout/AppHeader.tsx b/apps/website/components/layout/AppHeader.tsx index d3f8ef5e1..c70b9b098 100644 --- a/apps/website/components/layout/AppHeader.tsx +++ b/apps/website/components/layout/AppHeader.tsx @@ -1,72 +1,119 @@ 'use client'; -import { BrandMark } from '@/ui/BrandMark'; -import { HeaderActions } from '@/components/layout/HeaderActions'; -import { PublicNav } from '@/components/layout/PublicNav'; +import { Box } from '@/ui/Box'; +import { Text } from '@/ui/Text'; +import { Search, Bell, User, ChevronDown, Command } from 'lucide-react'; +import { usePathname } from 'next/navigation'; import { useCurrentSession } from '@/hooks/auth/useCurrentSession'; import { routes } from '@/lib/routing/RouteConfig'; -import { Box } from '@/ui/Box'; -import { Stack } from '@/ui/Stack'; -import { Text } from '@/ui/Text'; -import { Surface } from '@/ui/Surface'; -import { usePathname } from 'next/navigation'; +import { Button } from '@/ui/Button'; +import { useState, useEffect } from 'react'; +import { AppShellBar } from './AppShellBar'; export function AppHeader() { const pathname = usePathname(); const { data: session } = useCurrentSession(); const isAuthenticated = !!session; - const homeHref = isAuthenticated ? routes.protected.dashboard : routes.public.home; + + // Simple breadcrumb logic + const pathSegments = pathname.split('/').filter(Boolean); + const breadcrumbs = pathSegments.length > 0 + ? pathSegments.map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' / ') + : 'Home'; + + // Clock + const [time, setTime] = useState(''); + useEffect(() => { + const updateTime = () => { + const now = new Date(); + setTime(now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })); + }; + updateTime(); + const interval = setInterval(updateTime, 60000); + return () => clearInterval(interval); + }, []); return ( - - - {/* Left: Brand & Context */} - - - - {isAuthenticated && ( - - - Workspace - - - )} - + + {/* Left: Context & Search */} + + + {breadcrumbs} + - {/* Center: Navigation (if public) */} - {!isAuthenticated && ( - - + {/* Command Search - Refined */} + + + + + + K - )} - - {/* Right: Session Controls */} - - - - - LIVE - - - - + + {/* Right: System Status & User */} + + {/* System Time */} + + + {time} UTC + + + + {/* Notifications */} + + + + + + {/* User Menu */} + {isAuthenticated ? ( + + + + + + + {session.user.displayName || 'Driver'} + + + + + ) : ( + + + + + )} + + ); } diff --git a/apps/website/components/layout/AppShellBar.tsx b/apps/website/components/layout/AppShellBar.tsx new file mode 100644 index 000000000..c477905d2 --- /dev/null +++ b/apps/website/components/layout/AppShellBar.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Box } from '@/ui/Box'; +import { ReactNode } from 'react'; + +interface AppShellBarProps { + position: 'top' | 'bottom'; + children: ReactNode; + sidebarOffset?: boolean; +} + +export function AppShellBar({ position, children, sidebarOffset = true }: AppShellBarProps) { + const isTop = position === 'top'; + + return ( + + {children} + + ); +} diff --git a/apps/website/components/layout/AppSidebar.tsx b/apps/website/components/layout/AppSidebar.tsx index 827ba9c84..6659c73a9 100644 --- a/apps/website/components/layout/AppSidebar.tsx +++ b/apps/website/components/layout/AppSidebar.tsx @@ -1,50 +1,54 @@ 'use client'; -import { AuthedNav } from '@/components/layout/AuthedNav'; -import { PublicNav } from '@/components/layout/PublicNav'; -import { useCurrentSession } from '@/hooks/auth/useCurrentSession'; +import { BrandMark } from '@/ui/BrandMark'; +import { NavLink } from '@/ui/NavLink'; +import { routes } from '@/lib/routing/RouteConfig'; import { Box } from '@/ui/Box'; -import { DashboardRail } from '@/components/dashboard/DashboardRail'; -import { Text } from '@/ui/Text'; -import { Surface } from '@/ui/Surface'; +import { Stack } from '@/ui/Stack'; +import { + LayoutGrid, + Trophy, + Users, + Calendar, + Flag, + Home +} from 'lucide-react'; import { usePathname } from 'next/navigation'; export function AppSidebar() { const pathname = usePathname(); - const { data: session } = useCurrentSession(); - const isAuthenticated = !!session; + + const navItems = [ + { label: 'Home', href: routes.public.home, icon: Home }, + { label: 'Leagues', href: routes.public.leagues, icon: Trophy }, + { label: 'Drivers', href: routes.public.drivers, icon: Users }, + { label: 'Leaderboards', href: routes.public.leaderboards, icon: LayoutGrid }, + { label: 'Teams', href: routes.public.teams, icon: Flag }, + { label: 'Races', href: routes.public.races, icon: Calendar }, + ]; return ( - - - - - - - - DASHBOARD - - - - - {isAuthenticated ? ( - - ) : ( - - )} - - - - + + {/* Brand Header */} + + + + + {/* Navigation */} + + + {navItems.map((item) => ( + + ))} + + + ); } diff --git a/apps/website/components/layout/HeaderActions.tsx b/apps/website/components/layout/HeaderActions.tsx index f72569ae0..0803c4951 100644 --- a/apps/website/components/layout/HeaderActions.tsx +++ b/apps/website/components/layout/HeaderActions.tsx @@ -1,45 +1,49 @@ import { routes } from '@/lib/routing/RouteConfig'; import { Button } from '@/ui/Button'; import { Stack } from '@/ui/Stack'; -import { LogIn, UserPlus } from 'lucide-react'; +import { Box } from '@/ui/Box'; interface HeaderActionsProps { isAuthenticated: boolean; } -/** - * HeaderActions provides the primary actions in the header (Login, Signup, Profile). - */ export function HeaderActions({ isAuthenticated }: HeaderActionsProps) { if (isAuthenticated) { return ( - - - + ); } return ( - + + + + diff --git a/apps/website/components/layout/PublicNav.tsx b/apps/website/components/layout/PublicNav.tsx index d7168addf..5ca2a3ce0 100644 --- a/apps/website/components/layout/PublicNav.tsx +++ b/apps/website/components/layout/PublicNav.tsx @@ -1,6 +1,6 @@ import { routes } from '@/lib/routing/RouteConfig'; import { Stack } from '@/ui/Stack'; -import { Calendar, Home, Layout, Trophy, Users } from 'lucide-react'; +import { Calendar, Home, LayoutGrid, Trophy, Users, Flag } from 'lucide-react'; import { NavLink } from '@/ui/NavLink'; interface PublicNavProps { @@ -8,21 +8,18 @@ interface PublicNavProps { direction?: 'row' | 'col'; } -/** - * PublicNav displays navigation items for unauthenticated users. - */ export function PublicNav({ pathname, direction = 'col' }: PublicNavProps) { const items = [ { label: 'Home', href: routes.public.home, icon: Home }, { label: 'Leagues', href: routes.public.leagues, icon: Trophy }, { label: 'Drivers', href: routes.public.drivers, icon: Users }, - { label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout }, - { label: 'Teams', href: routes.public.teams, icon: Users }, + { label: 'Leaderboards', href: routes.public.leaderboards, icon: LayoutGrid }, + { label: 'Teams', href: routes.public.teams, icon: Flag }, { label: 'Races', href: routes.public.races, icon: Calendar }, ]; return ( - + {items.map((item) => ( } - sidebar={showSidebar ? : undefined} + sidebar={} footer={} - fixedHeader - fixedSidebar + fixedHeader={true} + fixedSidebar={true} + fixedFooter={false} > - {children} - + ); } diff --git a/apps/website/tsc_output.txt b/apps/website/tsc_output.txt new file mode 100644 index 000000000..1c0833c18 --- /dev/null +++ b/apps/website/tsc_output.txt @@ -0,0 +1,2 @@ +components/admin/AdminToolbar.tsx(24,7): error TS2322: Type '{ children: ReactNode; leftContent: ReactNode; variant: string; }' is not assignable to type 'IntrinsicAttributes & ControlBarProps'. + Property 'variant' does not exist on type 'IntrinsicAttributes & ControlBarProps'. diff --git a/apps/website/ui/BrandMark.tsx b/apps/website/ui/BrandMark.tsx index 0642aafd8..22c36691e 100644 --- a/apps/website/ui/BrandMark.tsx +++ b/apps/website/ui/BrandMark.tsx @@ -7,29 +7,20 @@ interface BrandMarkProps { priority?: boolean; } -/** - * BrandMark provides the consistent logo/wordmark for the application. - * Aligned with "Precision Racing Minimal" theme. - */ export function BrandMark({ href = '/' }: BrandMarkProps) { return ( - - - GridPilot - - + + + GridPilot diff --git a/apps/website/ui/Layout.tsx b/apps/website/ui/Layout.tsx index ff515d28c..35a355c6f 100644 --- a/apps/website/ui/Layout.tsx +++ b/apps/website/ui/Layout.tsx @@ -6,78 +6,71 @@ export interface LayoutProps { header?: ReactNode; footer?: ReactNode; sidebar?: ReactNode; - /** - * Whether the sidebar should be fixed to the side. - * If true, the main content will be offset by the sidebar width. - */ fixedSidebar?: boolean; - /** - * Whether the header should be fixed to the top. - * If true, the main content will be offset by the header height. - */ fixedHeader?: boolean; + fixedFooter?: boolean; } /** * Layout is the canonical app frame component. - * It orchestrates the high-level structure: Header, Sidebar, Main Content, and Footer. + * Redesigned for "Cockpit" layout: Sidebar is primary (full height), Header and Content sit to the right. */ export const Layout = ({ children, header, footer, sidebar, - fixedSidebar = false, - fixedHeader = false + fixedSidebar = true, + fixedHeader = true, + fixedFooter = true // Default to true for AppShellBar }: LayoutProps) => { return ( - - {header && ( + + {/* Sidebar - Primary Vertical Axis - Solid Background */} + {sidebar && ( - {header} + {sidebar} )} - - {sidebar && ( - - {sidebar} - - )} + {/* Main Content Area - Right of Sidebar */} + + {/* Header - Rendered directly as it contains AppShellBar (fixed) */} + {header} + {/* Main Scrollable Content */} - + {children} - - {footer && ( - - {footer} - - )} + + {/* Footer - Rendered directly as it contains AppShellBar (fixed) */} + {footer} ); diff --git a/apps/website/ui/NavLink.tsx b/apps/website/ui/NavLink.tsx index 6b884237a..03c781843 100644 --- a/apps/website/ui/NavLink.tsx +++ b/apps/website/ui/NavLink.tsx @@ -12,25 +12,56 @@ interface NavLinkProps { variant?: 'sidebar' | 'top'; } -/** - * NavLink provides a consistent link component for navigation. - * Supports both sidebar and top navigation variants. - */ export function NavLink({ href, label, icon, isActive, variant = 'sidebar' }: NavLinkProps) { + const isTop = variant === 'top'; + + // Dieter Rams style: Unobtrusive, Honest, Thorough. + // No glows. No shadows. Just clear contrast and alignment. + const content = ( - - {icon && } - + + {icon && ( + + )} + + {label} - {variant === 'sidebar' && isActive && ( - + + {/* Minimal Active Indicator */} + {!isTop && isActive && ( + )} ); return ( - + {content} ); diff --git a/docs/THEME.md b/docs/THEME.md index cad4698e7..18fa5b496 100644 --- a/docs/THEME.md +++ b/docs/THEME.md @@ -1,134 +1,172 @@ -🎨 GridPilot Theme — “Precision Racing Minimal” +GridPilot Theme — “Modern Precision with Obsessive Detail” -A clean, modern racing interface that feels like a cockpit dashboard — calm, sharp, and serious. +A meticulously crafted sim-racing interface built with absolute love to detail. ⸻ -1. Identity & Mood +Core Essence -GridPilot should feel like: - • a real motorsport instrument, not a game launcher - • calm and focused, like the moment before a qualifying lap - • precise, like a telemetry screen - • modern and minimal, without visual noise - • slightly futuristic, but never “RGB gamer chaos” +GridPilot should feel like a product where every pixel was placed on purpose. +A UI that radiates care, intention, and craft — the way a beautifully machined steering wheel or pedal set feels when you touch it. -It should appeal equally to sim racers, gamers, and casual fans. +Not loud. +Not flashy. +Not corporate. +But modern, obsessive, refined. + +The user should think: + +“Someone really cared when they designed this.” ⸻ -2. Visual Style +1. The Kind of Modern We Want -Core Aesthetic - • matte dark surfaces - • thin, crisp separators - • soft blue/cyan glows on interaction - • no aggressive neon - • subtle gradients for depth - • everything feels instrument-grade, not decorative +Modern ≠ trendy +GridPilot is modern in the way high-end hardware is modern: + • clean materials + • subtle gradients + • precision shadows + • invisible order + • absolute visual consistency -Color System - • Graphite Black — base background - • Charcoal — panel surfaces - • Steel Grey — separators & outlines - • Electric Blue — primary actions - • Telemetry Aqua — interactive highlights - • Motorsport Amber — warnings & signals +It feels engineered, not styled. -Colors should feel like motorsport, not corporate SaaS. +Everything is smooth, but never busy. +Everything is polished, but never glossy. +Everything is minimal, but never empty. + +It has the vibe of: + • Apple’s focus + • Porsche’s restraint + • Moza/Fanatec’s precision hardware aesthetics + • Telemetry dashboards + • Calm esports production graphics + +A blend of racing seriousness and premium digital craft. ⸻ -3. Components & Interaction +2. Love to Detail (the signature of GridPilot) -Panels & Cards - • slightly inset or raised - • reminiscent of cockpit modules - • structured and clean +This is where GridPilot stands out. -Tables - • information-dense - • instantly scannable - • light hover highlights - • telemetry-style status colors +Microspacing -Buttons - • flat by default - • glow only on hover - • snappy, “race-engineer” response speed +Spacing is not “good enough” — it’s perfectly balanced. +Margins breathe. +Rows align with intent. +Nothing floats randomly. -Modals - • soft frosted blur - • fast open/close - • subtle pit-lane lighting vibes +Typography finesse -⸻ +Numbers align sharply for standings. +Headings sit exactly on their baseline rhythm. +Secondary metadata uses softer brightness and tighter spacing. -4. Motion & Feedback +Shadow discipline -Motion should feel racing-inspired: - • short, crisp animations - • no bounce or playful movement - • hover = slight lift + color pulse - • loading = a thin progress line (pit limiter style) - • tab switching = sliding underline (chicane motion) +No random shadows — only a small, soft, controlled spread. +Layer depth is subtle but unmistakably refined. -Responsive, not playful. +Color temperature -⸻ +Dark modes often feel muddy or flat. +GridPilot should feel crisp, layered, and tuned — +as if the entire palette has been color-graded like broadcast graphics. -5. Layout Structure +Interactive texture -Think Telemetry Workspace: - • Sidebar → control rail - • Header → context + session controls - • Main Area → race tables, session details, track maps - • Right Panel → contextual info, drivers, actions - -Everything modular, readable, and effortless to navigate. - -⸻ - -6. Tone & Copywriting - -The product tone is: - • calm - • concise - • direct - • technical but human - • zero hype - -Examples: - • “Race added.” - • “Standings updated.” - • “Session ready.” - • “Review protest.” - • “Sponsor payout queued.” - -Never corporate. Never salesy. Never loud. - -⸻ - -7. Emotional Experience - -Users should feel: - • in control - • supported - • efficient - • connected to motorsport culture - • confident in the system - -A disciplined, trustworthy tool — not a flashy app. - -⸻ - -8. Overall Vibe - -“A premium cockpit dashboard — for people who actually race.” - • minimal +Every hover, highlight, or focus state is: + • soft + • smooth + • fast • precise - • clean - • serious - • attractive without noise -A tool you’re happy to use, because it respects your time and your craft. \ No newline at end of file +Nothing is bouncy, glowy, or game-y. +But everything feels alive when touched. + +Love to detail = you don’t notice it, but you feel it. + +⸻ + +3. Emotional Atmosphere + +The moment the UI loads, the user should feel: + • This is premium. + • This was crafted, not slapped together. + • This tool respects my time. + • This belongs to sim racing, not generic SaaS. + • This is serious but not sterile. + +It’s that mixture of calm confidence and quiet intensity that sim racers crave before entering a session. + +⸻ + +4. Visual Style Summary (crisp and modern) + +Surfaces + • matte + • low contrast + • ultra-clean edges + • subtle gradients only on interaction + +Accents + • electric blue or aqua, used sparingly + • amber for warnings (motorsport heritage) + • desaturated greys for hierarchy + +Lighting + • no neon + • no RGB vibes + • subtle glow only on very important interactive elements + +Depth + • controlled layering + • meaningful shadows + • frosted blur for modals (garage glass vibe) + +⸻ + +5. Motion + +Animations reflect precision: + • fast acceleration + • quick settle + • no jitter + • no wasted frames + +Transitions should feel like: + • opening a garage screen + • switching telemetry pages + • selecting a gear cleanly + +A sense of mechanical smoothness. + +⸻ + +6. Why This Theme Works for Our Audience + +For gamers: + • visually immersive + • polished like a AAA menu + • subtle effects that feel “alive” + +For sim racers: + • data clarity + • racing-inspired accents + • tool-like seriousness + +For devs: + • clean structure + • intentional simplicity + • maintainable aesthetic rules + +This theme hits the sweet spot: +professional, modern, crafted, premium. + +⸻ + +One-Line Summary + +GridPilot should feel like a high-end sim racing cockpit UI — obsessively detailed, modern, calm, and deeply cared for, where every interaction feels engineered with precision. \ No newline at end of file