From e9dfe15dff1c6de33f265c9e19b468731c74aae0 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 20 Jan 2026 00:33:24 +0100 Subject: [PATCH] website refactor --- apps/website/components/dev/DevToolbar.tsx | 2 +- apps/website/components/layout/AppFooter.tsx | 55 +++---- apps/website/components/layout/AppHeader.tsx | 138 +++++++----------- .../website/components/layout/AppShellBar.tsx | 17 ++- apps/website/components/layout/AppSidebar.tsx | 93 +++++++++--- .../components/layout/CommandModal.tsx | 70 +++++++++ .../components/layout/SidebarContext.tsx | 48 ++++++ apps/website/components/profile/UserPill.tsx | 26 ++-- .../templates/layout/RootAppShellTemplate.tsx | 35 +++-- apps/website/ui/BrandMark.tsx | 18 +-- apps/website/ui/Layout.tsx | 18 ++- apps/website/ui/NavLink.tsx | 80 +++++----- 12 files changed, 376 insertions(+), 224 deletions(-) create mode 100644 apps/website/components/layout/CommandModal.tsx create mode 100644 apps/website/components/layout/SidebarContext.tsx diff --git a/apps/website/components/dev/DevToolbar.tsx b/apps/website/components/dev/DevToolbar.tsx index a55ea5269..c6461c4d1 100644 --- a/apps/website/components/dev/DevToolbar.tsx +++ b/apps/website/components/dev/DevToolbar.tsx @@ -245,7 +245,7 @@ export function DevToolbar() { (''); + 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: System Info */} - + - GRIDPILOT OS v2.0 + GRIDPILOT - - © {currentYear} + + + {/* Center: Time */} + + + {time} UTC - {/* Center: Telemetry Status */} - - - - - - - - 12ms - - - {/* Right: Legal & Tools */} - + Terms Privacy Status @@ -46,19 +50,6 @@ export function AppFooter() { ); } -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 ( s.charAt(0).toUpperCase() + s.slice(1)).join(' / ') : 'Home'; - // Clock - const [time, setTime] = useState(''); + // Cmd+K Listener useEffect(() => { - const updateTime = () => { - const now = new Date(); - setTime(now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })); + const down = (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setIsCommandOpen((open) => !open); + } }; - updateTime(); - const interval = setInterval(updateTime, 60000); - return () => clearInterval(interval); + document.addEventListener('keydown', down); + return () => document.removeEventListener('keydown', down); }, []); return ( - - {/* Left: Context & Search */} - - - {breadcrumbs} - - - {/* Command Search - Refined */} - - - - - - K - - - - - {/* Right: System Status & User */} - - {/* System Time */} - - - {time} UTC + <> + + {/* Left: Context & Search */} + + + {breadcrumbs} + + {/* Command Search Trigger */} + - {/* Notifications */} - - - - - - {/* User Menu */} - {isAuthenticated ? ( - + {/* Right: User & Notifications */} + + {/* Notifications - Only when authed */} + {isAuthenticated && ( - + + - - - {session.user.displayName || 'Driver'} - - - - - ) : ( - - - - - )} - - + )} + + {/* User Pill (Handles Auth & Menu) */} + + + + + setIsCommandOpen(false)} /> + ); } diff --git a/apps/website/components/layout/AppShellBar.tsx b/apps/website/components/layout/AppShellBar.tsx index c477905d2..40ec9b953 100644 --- a/apps/website/components/layout/AppShellBar.tsx +++ b/apps/website/components/layout/AppShellBar.tsx @@ -2,6 +2,7 @@ import { Box } from '@/ui/Box'; import { ReactNode } from 'react'; +import { useSidebar } from '@/components/layout/SidebarContext'; interface AppShellBarProps { position: 'top' | 'bottom'; @@ -11,22 +12,24 @@ interface AppShellBarProps { export function AppShellBar({ position, children, sidebarOffset = true }: AppShellBarProps) { const isTop = position === 'top'; + const { isCollapsed } = useSidebar(); + const leftClass = sidebarOffset + ? (isCollapsed ? 'lg:left-20' : 'lg:left-64') + : 'left-0'; + return ( {children} diff --git a/apps/website/components/layout/AppSidebar.tsx b/apps/website/components/layout/AppSidebar.tsx index 6659c73a9..2e080e03b 100644 --- a/apps/website/components/layout/AppSidebar.tsx +++ b/apps/website/components/layout/AppSidebar.tsx @@ -5,50 +5,107 @@ import { NavLink } from '@/ui/NavLink'; import { routes } from '@/lib/routing/RouteConfig'; import { Box } from '@/ui/Box'; import { Stack } from '@/ui/Stack'; +import { Text } from '@/ui/Text'; import { LayoutGrid, Trophy, Users, Calendar, Flag, - Home + Home, + ChevronLeft, + ChevronRight } from 'lucide-react'; import { usePathname } from 'next/navigation'; +import { useSidebar } from '@/components/layout/SidebarContext'; export function AppSidebar() { const pathname = usePathname(); + const { isCollapsed, toggleCollapse } = useSidebar(); - const navItems = [ + const mainItems = [ { 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 }, + ]; + + const competitionItems = [ { label: 'Races', href: routes.public.races, icon: Calendar }, + { label: 'Leaderboards', href: routes.public.leaderboards, icon: LayoutGrid }, ]; return ( - + {/* Brand Header */} - - + + {/* Navigation */} - - - {navItems.map((item) => ( - - ))} + + + + {!isCollapsed && ( + + Platform + + )} + + {mainItems.map((item) => ( + + ))} + + + + + {!isCollapsed && ( + + Competition + + )} + + {competitionItems.map((item) => ( + + ))} + + + + {/* Bottom Actions */} + + + ); } diff --git a/apps/website/components/layout/CommandModal.tsx b/apps/website/components/layout/CommandModal.tsx new file mode 100644 index 000000000..08d61ac28 --- /dev/null +++ b/apps/website/components/layout/CommandModal.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { Modal } from '@/components/shared/Modal'; +import { Box } from '@/ui/Box'; +import { Text } from '@/ui/Text'; +import { Search, Command, ArrowRight } from 'lucide-react'; +import { useState, useEffect } from 'react'; + +interface CommandModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function CommandModal({ isOpen, onClose }: CommandModalProps) { + const [query, setQuery] = useState(''); + + // Mock results + const results = [ + { label: 'Go to Dashboard', shortcut: 'G D' }, + { label: 'Find Driver...', shortcut: 'Cmd F' }, + { label: 'Create League', shortcut: 'C L' }, + ].filter(r => r.label.toLowerCase().includes(query.toLowerCase())); + + return ( + } + > + + + + setQuery(e.target.value)} + /> + + + + + Suggestions + + {results.map((result, i) => ( + + ))} + + + + ); +} diff --git a/apps/website/components/layout/SidebarContext.tsx b/apps/website/components/layout/SidebarContext.tsx new file mode 100644 index 000000000..1517d916a --- /dev/null +++ b/apps/website/components/layout/SidebarContext.tsx @@ -0,0 +1,48 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect } from 'react'; + +interface SidebarContextType { + isCollapsed: boolean; + toggleCollapse: () => void; + setCollapsed: (collapsed: boolean) => void; +} + +const SidebarContext = createContext(undefined); + +export function SidebarProvider({ children }: { children: React.ReactNode }) { + const [isCollapsed, setIsCollapsed] = useState(false); + + // Persist state (optional, but good for UX) + useEffect(() => { + const stored = localStorage.getItem('sidebar-collapsed'); + if (stored) setIsCollapsed(stored === 'true'); + }, []); + + const toggleCollapse = () => { + setIsCollapsed((prev) => { + const next = !prev; + localStorage.setItem('sidebar-collapsed', String(next)); + return next; + }); + }; + + const setCollapsed = (collapsed: boolean) => { + setIsCollapsed(collapsed); + localStorage.setItem('sidebar-collapsed', String(collapsed)); + }; + + return ( + + {children} + + ); +} + +export function useSidebar() { + const context = useContext(SidebarContext); + if (context === undefined) { + throw new Error('useSidebar must be used within a SidebarProvider'); + } + return context; +} diff --git a/apps/website/components/profile/UserPill.tsx b/apps/website/components/profile/UserPill.tsx index f0abcbfa7..8403da4ed 100644 --- a/apps/website/components/profile/UserPill.tsx +++ b/apps/website/components/profile/UserPill.tsx @@ -147,20 +147,18 @@ export function UserPill() { // Handle unauthenticated users if (!session) { return ( - - - Sign In - - - Get Started - - + + + Enter GridPilot + + + + + ); } diff --git a/apps/website/templates/layout/RootAppShellTemplate.tsx b/apps/website/templates/layout/RootAppShellTemplate.tsx index f41f79775..f189bd920 100644 --- a/apps/website/templates/layout/RootAppShellTemplate.tsx +++ b/apps/website/templates/layout/RootAppShellTemplate.tsx @@ -5,6 +5,7 @@ import { AppHeader } from '@/components/layout/AppHeader'; import { AppSidebar } from '@/components/layout/AppSidebar'; import { Layout } from '@/ui/Layout'; import { Box } from '@/ui/Box'; +import { SidebarProvider } from '@/components/layout/SidebarContext'; import React from 'react'; export interface RootAppShellViewData { @@ -17,22 +18,24 @@ export interface RootAppShellViewData { */ export function RootAppShellTemplate({ children }: RootAppShellViewData) { return ( - } - sidebar={} - footer={} - fixedHeader={true} - fixedSidebar={true} - fixedFooter={false} - > - + } + sidebar={} + footer={} + fixedHeader={true} + fixedSidebar={true} + fixedFooter={true} > - {children} - - + + {children} + + + ); } diff --git a/apps/website/ui/BrandMark.tsx b/apps/website/ui/BrandMark.tsx index 22c36691e..418edcf6f 100644 --- a/apps/website/ui/BrandMark.tsx +++ b/apps/website/ui/BrandMark.tsx @@ -1,26 +1,22 @@ import { Box } from '@/ui/Box'; -import { Link } from '@/ui/Link'; import { Image } from '@/ui/Image'; +import { Link } from '@/ui/Link'; interface BrandMarkProps { href?: string; priority?: boolean; + variant?: 'dark' | 'light'; + collapsed?: boolean; } -export function BrandMark({ href = '/' }: BrandMarkProps) { +export function BrandMark({ href = '/', collapsed = false }: BrandMarkProps) { return ( - + - - GridPilot diff --git a/apps/website/ui/Layout.tsx b/apps/website/ui/Layout.tsx index 35a355c6f..4c560aaaf 100644 --- a/apps/website/ui/Layout.tsx +++ b/apps/website/ui/Layout.tsx @@ -1,5 +1,6 @@ import { ReactNode } from 'react'; -import { Box } from './Box'; +import { Box } from '@/ui/Box'; +import { useSidebar } from '@/components/layout/SidebarContext'; export interface LayoutProps { children: ReactNode; @@ -22,22 +23,27 @@ export const Layout = ({ sidebar, fixedSidebar = true, fixedHeader = true, - fixedFooter = true // Default to true for AppShellBar + fixedFooter = true }: LayoutProps) => { + const { isCollapsed } = useSidebar(); + const sidebarWidth = isCollapsed ? '20' : '64'; // 5rem vs 16rem + const sidebarWidthClass = isCollapsed ? 'lg:w-20' : 'lg:w-64'; + const contentMarginClass = isCollapsed ? 'lg:ml-20' : 'lg:ml-64'; + return ( {/* Sidebar - Primary Vertical Axis - Solid Background */} {sidebar && ( {sidebar} @@ -48,7 +54,7 @@ export const Layout = ({ display="flex" flexDirection="col" flex={1} - marginLeft={fixedSidebar && sidebar ? { lg: 64 } : undefined} + className={`transition-all duration-300 ease-in-out ${fixedSidebar && sidebar ? contentMarginClass : ''}`} minWidth="0" // Prevent flex child overflow > {/* Header - Rendered directly as it contains AppShellBar (fixed) */} diff --git a/apps/website/ui/NavLink.tsx b/apps/website/ui/NavLink.tsx index 03c781843..6db44d784 100644 --- a/apps/website/ui/NavLink.tsx +++ b/apps/website/ui/NavLink.tsx @@ -1,8 +1,8 @@ import { Box } from '@/ui/Box'; import { Icon } from '@/ui/Icon'; -import { Link } from '@/ui/Link'; +import Link from 'next/link'; import { Text } from '@/ui/Text'; -import { LucideIcon } from 'lucide-react'; +import { LucideIcon, ChevronRight } from 'lucide-react'; interface NavLinkProps { href: string; @@ -10,58 +10,72 @@ interface NavLinkProps { icon?: LucideIcon; isActive?: boolean; variant?: 'sidebar' | 'top'; + collapsed?: boolean; } -export function NavLink({ href, label, icon, isActive, variant = 'sidebar' }: NavLinkProps) { +export function NavLink({ href, label, icon, isActive, variant = 'sidebar', collapsed = false }: NavLinkProps) { const isTop = variant === 'top'; - // Dieter Rams style: Unobtrusive, Honest, Thorough. - // No glows. No shadows. Just clear contrast and alignment. + // Radical "Game Menu" Style + // Active: Solid Primary Color, Shadow, Bold. + // Inactive: Glass card with strong hover effects. const content = ( - {icon && ( - - )} - - - {label} - + + {icon && ( + + )} + + {!collapsed && ( + + {label} + + )} + - {/* Minimal Active Indicator */} - {!isTop && isActive && ( - )} ); return ( - + {content} );