354 lines
17 KiB
TypeScript
354 lines
17 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="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="/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 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 & 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="/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>
|
|
);
|
|
}
|