MotorSignal-App/website/components/site-header-client.tsx

385 lines
19 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface SiteHeaderProps {
signedIn: boolean;
role: string;
}
export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [userMenuOpen, setUserMenuOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setMobileMenuOpen(false);
setUserMenuOpen(false);
}, [pathname]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
setUserMenuOpen(false);
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
setUserMenuOpen(false);
}
}
if (userMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
};
}, [userMenuOpen]);
const isClubManageInterior = pathname !== '/clubs/manage' && Boolean(pathname?.startsWith('/clubs/') && pathname?.includes('/manage'));
if (isClubManageInterior || pathname?.startsWith('/admin') || pathname?.startsWith('/ops')) return null;
const isAdmin = role === 'admin' || role === 'operator' || role === 'owner';
const navItems = [
{ label: 'Live Cockpit', href: '/' },
{ label: 'Features', href: '/features' },
{ label: 'Group Rides', href: '/group-rides' },
{ label: 'Clubs', href: '/clubs' },
{ label: 'Pricing', href: '/pricing' },
{ label: 'Hardware & App', href: '/pricing#hardware' },
];
return (
<header className="fixed top-0 w-full z-50 bg-surface-container-lowest/80 backdrop-blur-xl border-b border-outline-variant/30 shadow-[0_1px_8px_rgba(0,0,0,0.04)]">
<div className="h-20 max-w-container-max mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between gap-space-sm sm:gap-space-md">
{/* Left: Brand + Navigation */}
<div className="flex items-center gap-space-sm sm:gap-space-lg min-w-0">
<Link className="flex items-center gap-2 sm:gap-space-sm group shrink-0" href="/" aria-label="MotorSignal home">
<div className="w-9 h-9 sm:w-10 sm:h-10 rounded-xl bg-gradient-to-tr from-primary-container via-surface-tint to-tertiary-container flex items-center justify-center shadow-[0_0_16px_rgba(0,242,254,0.35)] shrink-0">
<span className="material-symbols-outlined text-on-primary-container text-[20px] sm:text-[22px] font-bold">show_chart</span>
</div>
<div className="flex flex-col min-w-0">
<span className="font-headline-sm text-sm sm:text-headline-sm text-on-surface tracking-tight group-hover:text-primary-container transition-colors truncate">MotorSignal</span>
<span className="font-label-caps text-[10px] sm:text-label-caps text-primary-fixed-dim tracking-normal hidden xs:block">Pro Telemetry</span>
</div>
</Link>
<nav className="hidden xl:flex items-center gap-space-lg whitespace-nowrap" aria-label="Primary navigation">
{navItems.map((item) => {
const isActive =
item.href === '/'
? pathname === '/'
: item.href === '/clubs'
? pathname === '/clubs' || (pathname.startsWith('/clubs/') && !pathname.includes('/manage'))
: pathname.startsWith(item.href);
return (
<Link
key={item.label}
href={item.href}
className={`transition-colors whitespace-nowrap ${
isActive
? 'text-primary-container font-headline-sm border-b-2 border-primary-container pb-0.5'
: 'font-label-md text-label-md text-on-surface-variant hover:text-on-surface'
}`}
>
{item.label}
</Link>
);
})}
</nav>
</div>
{/* Right: Telemetry Grid pill + Auth actions + Mobile Hamburger */}
<div className="flex items-center gap-1.5 sm:gap-space-md whitespace-nowrap shrink-0">
{/* Telemetry Grid: Online Badge */}
<div className="hidden md:flex items-center gap-space-xs px-space-sm py-1 rounded-full bg-surface-container-low border border-outline-variant/40">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-secondary-fixed-dim"></span>
</span>
<span className="font-label-caps text-label-caps text-on-surface-variant">
Telemetry Grid: Online
</span>
</div>
{signedIn ? (
<div className="flex items-center gap-2">
<Link
href="/group-rides"
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-surface-container-high hover:bg-surface-container border border-outline-variant/30 text-xs font-semibold text-on-surface hover:text-primary-container transition-colors"
title="Group Rides Hub"
>
<span className="material-symbols-outlined text-[16px] text-primary-container">groups</span>
<span>Group Rides</span>
</Link>
<div className="relative" ref={userMenuRef}>
<button
type="button"
onClick={() => setUserMenuOpen(!userMenuOpen)}
className={`w-9 h-9 sm:w-11 sm:h-11 rounded-full bg-surface-container-high border border-outline-variant/40 flex items-center justify-center transition-all shrink-0 cursor-pointer ${
userMenuOpen
? 'ring-2 ring-primary-container bg-surface-container shadow-[0_0_16px_rgba(0,242,254,0.25)]'
: 'hover:ring-2 hover:ring-primary-container'
}`}
title="Rider Menu"
aria-label="Open rider menu"
aria-expanded={userMenuOpen}
aria-haspopup="menu"
>
<span className="material-symbols-outlined text-primary-container text-[20px] sm:text-[22px]">
person
</span>
</button>
{userMenuOpen && (
<div
role="menu"
aria-orientation="vertical"
className="absolute right-0 mt-2 w-64 rounded-2xl bg-surface-container-lowest/95 backdrop-blur-2xl border border-outline-variant/40 shadow-2xl p-2 z-50 animate-in fade-in slide-in-from-top-2 flex flex-col gap-1"
>
<div className="px-3 py-2 border-b border-outline-variant/20 mb-1">
<span className="font-headline-sm text-xs font-semibold text-on-surface block">
Rider Account
</span>
<span className="font-label-caps text-[10px] text-primary-fixed-dim">
{isAdmin ? 'Staff / Administrator' : 'Telemetry Connected'}
</span>
</div>
<Link
href="/account"
onClick={() => setUserMenuOpen(false)}
role="menuitem"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface hover:text-primary-container hover:bg-surface-container transition-colors text-xs font-semibold"
>
<span className="material-symbols-outlined text-[18px] text-primary-container">
person
</span>
<span>Rider Account</span>
</Link>
<Link
href="/group-rides"
onClick={() => setUserMenuOpen(false)}
role="menuitem"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface hover:text-primary-container hover:bg-surface-container transition-colors text-xs font-semibold"
>
<span className="material-symbols-outlined text-[18px] text-primary-container">
groups
</span>
<span>Group Rides</span>
</Link>
<Link
href="/clubs/manage"
onClick={() => setUserMenuOpen(false)}
role="menuitem"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface hover:text-secondary hover:bg-surface-container transition-colors text-xs font-semibold"
>
<span className="material-symbols-outlined text-[18px] text-secondary">
shield
</span>
<span>Founder Portal</span>
</Link>
{isAdmin && (
<Link
href="/admin"
onClick={() => setUserMenuOpen(false)}
role="menuitem"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface hover:text-primary-container hover:bg-surface-container transition-colors text-xs font-semibold"
>
<span className="material-symbols-outlined text-[18px] text-primary-container">
admin_panel_settings
</span>
<span>Operations Admin</span>
</Link>
)}
<div className="border-t border-outline-variant/20 my-1" />
<button
type="button"
role="menuitem"
onClick={async () => {
setUserMenuOpen(false);
try {
await fetch('/api/auth/logout', { method: 'POST' });
} catch {}
try {
const { firebaseAuth } = await import('../lib/firebase-client');
await firebaseAuth.signOut();
} catch {}
window.location.assign('/login');
}}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-error/80 hover:text-error hover:bg-error-container/10 transition-colors text-xs font-semibold text-left cursor-pointer"
>
<span className="material-symbols-outlined text-[18px]">logout</span>
<span>Sign Out</span>
</button>
</div>
)}
</div>
</div>
) : (
<div className="flex items-center gap-1.5 sm:gap-space-sm">
<Link
className="hidden sm:inline-flex items-center justify-center px-space-md py-2.5 rounded-xl bg-surface-container-low hover:bg-surface-container-high border border-outline-variant/40 text-on-surface hover:text-primary transition-all font-label-md text-label-md min-h-[44px]"
href="/login"
>
Sign In
</Link>
<Link
className="inline-flex items-center justify-center px-2.5 sm:px-space-lg py-2 sm:py-2.5 rounded-xl bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-xs sm:text-headline-sm hover:shadow-[0_0_24px_rgba(0,242,254,0.45)] transition-all whitespace-nowrap min-h-[38px] sm:min-h-[44px]"
href="/register"
>
<span className="hidden sm:inline">Get Started Free</span>
<span className="sm:hidden">Get Started</span>
</Link>
<Link
className="sm:hidden w-9 h-9 rounded-full bg-primary flex items-center justify-center hover:ring-2 hover:ring-primary-container transition-all shrink-0"
href="/login"
title="Account"
aria-label="Sign in"
>
<span className="material-symbols-outlined text-on-primary text-[18px]">
person
</span>
</Link>
</div>
)}
{/* Mobile Menu Hamburger Button */}
<button
type="button"
className="xl:hidden w-9 h-9 sm:w-11 sm:h-11 rounded-xl bg-surface-container-low hover:bg-surface-container-high border border-outline-variant/40 flex items-center justify-center text-on-surface hover:text-primary transition-colors cursor-pointer shrink-0"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label={mobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}
aria-expanded={mobileMenuOpen}
>
<span className="material-symbols-outlined text-[20px] sm:text-[24px]">
{mobileMenuOpen ? 'close' : 'menu'}
</span>
</button>
</div>
</div>
{/* Mobile Menu Drawer */}
{mobileMenuOpen && (
<div className="xl:hidden bg-surface-container-lowest/95 backdrop-blur-2xl border-b border-outline-variant/40 px-4 sm:px-gutter-desktop py-space-md shadow-2xl flex flex-col gap-space-sm animate-in slide-in-from-top-2">
<nav className="flex flex-col gap-1" aria-label="Mobile navigation">
{navItems.map((item) => {
const isActive =
item.href === '/'
? pathname === '/'
: item.href === '/clubs'
? pathname === '/clubs' || (pathname.startsWith('/clubs/') && !pathname.includes('/manage'))
: pathname.startsWith(item.href);
return (
<Link
key={item.label}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
className={`px-space-md py-3 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center ${
isActive
? 'bg-surface-container-high text-primary-container font-semibold border-l-4 border-primary-container'
: 'text-on-surface-variant hover:text-on-surface hover:bg-surface-container'
}`}
>
{item.label}
</Link>
);
})}
</nav>
{signedIn ? (
<div className="pt-2 border-t border-outline-variant/20 flex flex-col gap-1">
<span className="px-space-md py-1 font-mono text-[10px] text-outline font-semibold">
Rider Account &amp; Management
</span>
<Link
href="/account"
onClick={() => setMobileMenuOpen(false)}
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-on-surface hover:bg-surface-container"
>
<span className="material-symbols-outlined text-[20px] text-primary-container">person</span>
<span>Rider Account</span>
</Link>
<Link
href="/group-rides"
onClick={() => setMobileMenuOpen(false)}
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-on-surface hover:bg-surface-container"
>
<span className="material-symbols-outlined text-[20px] text-primary-container">groups</span>
<span>Group Rides</span>
</Link>
<Link
href="/clubs/manage"
onClick={() => setMobileMenuOpen(false)}
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-on-surface hover:bg-surface-container"
>
<span className="material-symbols-outlined text-[20px] text-secondary">shield</span>
<span>Founder Portal</span>
</Link>
{isAdmin && (
<Link
href="/admin"
onClick={() => setMobileMenuOpen(false)}
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-primary-container bg-surface-container hover:bg-surface-container-high border border-outline-variant/30"
>
<span className="material-symbols-outlined text-[20px]">admin_panel_settings</span>
<span>Operations Admin</span>
</Link>
)}
<button
type="button"
onClick={async () => {
setMobileMenuOpen(false);
try {
await fetch('/api/auth/logout', { method: 'POST' });
} catch {}
try {
const { firebaseAuth } = await import('../lib/firebase-client');
await firebaseAuth.signOut();
} catch {}
window.location.assign('/login');
}}
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-error/80 hover:text-error hover:bg-error-container/10 text-left cursor-pointer"
>
<span className="material-symbols-outlined text-[20px]">logout</span>
<span>Sign Out</span>
</button>
</div>
) : (
<div className="pt-space-sm border-t border-outline-variant/30 flex items-center justify-between text-xs">
<div className="flex items-center gap-space-xs">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-secondary-fixed-dim"></span>
</span>
<span className="font-label-caps text-label-caps text-on-surface-variant">
Telemetry Grid: Online
</span>
</div>
<Link
href="/login"
onClick={() => setMobileMenuOpen(false)}
className="text-primary font-label-md text-label-md hover:underline py-1.5 px-2"
>
Sign In →
</Link>
</div>
)}
</div>
)}
</header>
);
}