317 lines
14 KiB
TypeScript
317 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { Club, UserPortalPermissions, ClubRole } from './types';
|
|
|
|
interface FounderTopBarProps {
|
|
club: Club;
|
|
permissions: UserPortalPermissions;
|
|
activeNav?: string;
|
|
onToggleMobileSidebar?: () => void;
|
|
onPlanRide?: () => void;
|
|
onPostAnnouncement?: () => void;
|
|
onSwitchRole?: (role: ClubRole) => void;
|
|
onSwitchClub?: () => void;
|
|
}
|
|
|
|
export function FounderTopBar({
|
|
club,
|
|
permissions,
|
|
activeNav,
|
|
onToggleMobileSidebar,
|
|
onPlanRide,
|
|
onPostAnnouncement,
|
|
onSwitchRole,
|
|
onSwitchClub,
|
|
}: FounderTopBarProps) {
|
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
const userMenuRef = useRef<HTMLDivElement>(null);
|
|
|
|
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]);
|
|
|
|
return (
|
|
<header className="fixed top-0 left-0 lg:left-72 right-0 h-16 bg-surface-container-lowest/95 backdrop-blur-md border-b border-brand-border/60 z-40">
|
|
<div className="w-full max-w-[1536px] mx-auto h-full px-6 sm:px-8 lg:px-8 flex items-center justify-between gap-3 sm:gap-4">
|
|
{/* Left: Mobile hamburger + Breadcrumbs + Switch Club */}
|
|
<div className="flex items-center gap-2.5 sm:gap-4 min-w-0">
|
|
{/* Mobile Hamburger Toggle */}
|
|
<button
|
|
onClick={onToggleMobileSidebar}
|
|
className="lg:hidden text-outline hover:text-on-surface p-1 rounded-lg hover:bg-surface-container transition-colors shrink-0"
|
|
aria-label="Open sidebar"
|
|
>
|
|
<span className="material-symbols-outlined text-[22px]">menu</span>
|
|
</button>
|
|
|
|
<div className="flex items-center gap-1.5 sm:gap-2 font-mono text-xs text-outline min-w-0">
|
|
<Link href="/clubs" className="hover:text-on-surface transition-colors shrink-0">
|
|
Clubs
|
|
</Link>
|
|
<span className="shrink-0">/</span>
|
|
<Link
|
|
href={`/clubs/${club.slug || club.id}/manage`}
|
|
className="text-on-surface font-semibold truncate max-w-[110px] sm:max-w-[160px] lg:max-w-[140px] xl:max-w-none hover:text-brand-cyan transition-colors"
|
|
>
|
|
{club.name}
|
|
</Link>
|
|
{activeNav === 'profile' && (
|
|
<>
|
|
<span className="text-outline-variant shrink-0">/</span>
|
|
<span className="text-brand-cyan font-semibold truncate">Club Profile</span>
|
|
</>
|
|
)}
|
|
{activeNav === 'applications' && (
|
|
<>
|
|
<span className="text-outline-variant shrink-0">/</span>
|
|
<span data-breadcrumb="APPLICATIONS" className="text-brand-cyan font-semibold truncate">Applications</span>
|
|
</>
|
|
)}
|
|
{activeNav === 'ranks_roles' && (
|
|
<>
|
|
<span className="text-outline-variant shrink-0">/</span>
|
|
<span className="text-brand-cyan font-semibold truncate">Ranks & Roles</span>
|
|
</>
|
|
)}
|
|
{activeNav === 'rides' && (
|
|
<>
|
|
<span className="text-outline-variant shrink-0">/</span>
|
|
<span data-breadcrumb="RIDES" className="text-brand-cyan font-semibold truncate">Rides</span>
|
|
</>
|
|
)}
|
|
{activeNav === 'settings' && (
|
|
<>
|
|
<span className="text-outline-variant shrink-0">/</span>
|
|
<span className="text-brand-cyan font-semibold truncate">Settings</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="relative flex items-center shrink-0">
|
|
<button
|
|
onClick={onSwitchClub}
|
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-surface-container border border-brand-border/60 text-slate-200 font-body text-xs hover:bg-surface-container-high transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-[15px] text-brand-cyan">swap_horiz</span>
|
|
<span className="hidden sm:inline lg:hidden xl:inline">Switch Club</span>
|
|
<span className="material-symbols-outlined text-[15px] text-outline hidden sm:inline lg:hidden xl:inline">expand_more</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Middle: Search Input (desktop/tablet only, hidden on lg to prevent crush) */}
|
|
<div className="flex-1 max-w-xs mx-2 lg:mx-4 min-w-0 hidden md:block lg:hidden xl:block">
|
|
<div className="relative">
|
|
<span className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-[17px]">
|
|
search
|
|
</span>
|
|
<input
|
|
className="w-full h-8 pl-9 pr-3 rounded-lg bg-surface-container-low border border-brand-border/60 text-on-surface placeholder:text-outline/70 font-body text-xs focus:outline-none focus:border-brand-cyan transition-colors"
|
|
placeholder="Search members, rides, routes..."
|
|
type="text"
|
|
aria-label="Search members, rides, routes"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Actions + Role Switcher preview pill + Notifications + Profile */}
|
|
<div className="flex items-center gap-2 sm:gap-2.5 shrink-0 flex-wrap justify-end">
|
|
{/* Authoritative Role Badge (Production) vs Interactive Role Switcher (Development Only) */}
|
|
{process.env.NODE_ENV === 'development' ? (
|
|
<div
|
|
className="hidden 2xl:flex items-center gap-1 px-2 py-0.5 rounded-lg bg-surface-container-high border border-brand-border/60 text-[11px] font-mono text-outline"
|
|
title="Interactive role preview for testing software permissions (Development Only)"
|
|
>
|
|
<span className="text-outline-variant">Dev Preview:</span>
|
|
{(['owner', 'admin', 'member'] as const).map((r) => (
|
|
<button
|
|
key={r}
|
|
onClick={() => onSwitchRole?.(r)}
|
|
className={`px-1.5 py-0.5 rounded capitalize font-semibold transition-colors ${
|
|
permissions.role === r
|
|
? 'bg-primary-container text-on-primary-container font-bold'
|
|
: 'hover:text-on-surface'
|
|
}`}
|
|
>
|
|
{r}
|
|
</button>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="hidden sm:flex lg:hidden xl:flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-surface-container border border-brand-border/60 text-[11px] font-mono text-outline capitalize font-medium"
|
|
title="Authoritative software role"
|
|
>
|
|
<span className="w-1.5 h-1.5 rounded-full bg-brand-cyan" />
|
|
<span className="text-on-surface font-semibold">{permissions.role}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Post Announcement (Owner / Admin only) */}
|
|
{permissions.canManageSettings && (
|
|
<button
|
|
onClick={onPostAnnouncement}
|
|
className="hidden xl:flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-surface-container border border-brand-border/60 text-slate-200 font-body text-xs font-medium hover:bg-surface-container-high transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-[15px] text-brand-cyan">campaign</span>
|
|
<span>Post Announcement</span>
|
|
</button>
|
|
)}
|
|
|
|
{/* Plan Club Ride (Owner / Admin only) */}
|
|
{permissions.canPlanRides && (
|
|
<button
|
|
onClick={onPlanRide}
|
|
className="flex items-center gap-1.5 px-3 sm:px-3.5 py-1.5 rounded-xl bg-primary-container text-on-primary-container font-body text-xs font-bold hover:brightness-110 shadow-[0_0_12px_rgba(0,242,254,0.25)] transition-all shrink-0"
|
|
>
|
|
<span className="material-symbols-outlined text-[15px]">add_road</span>
|
|
<span className="hidden sm:inline lg:hidden xl:inline">Plan Club Ride</span>
|
|
</button>
|
|
)}
|
|
|
|
{/* Notifications */}
|
|
<div className="relative ml-0.5 sm:ml-1 shrink-0">
|
|
<button
|
|
className="w-8 h-8 rounded-xl bg-surface-container border border-brand-border/60 flex items-center justify-center text-outline hover:text-on-surface transition-colors"
|
|
title="Notifications"
|
|
aria-label="Notifications"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">notifications</span>
|
|
</button>
|
|
<span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-error" />
|
|
</div>
|
|
|
|
{/* Rider Profile Menu Dropdown */}
|
|
<div className="relative shrink-0 ml-0.5" ref={userMenuRef}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
|
className={`w-8 h-8 rounded-xl bg-surface-container-low hover:bg-surface-container-high border transition-all flex items-center justify-center cursor-pointer ${
|
|
userMenuOpen
|
|
? 'border-primary-container ring-2 ring-primary-container/20'
|
|
: 'border-brand-border/60 hover:border-brand-cyan/50'
|
|
}`}
|
|
title="Rider Menu"
|
|
aria-label="Open rider menu"
|
|
aria-expanded={userMenuOpen}
|
|
aria-haspopup="menu"
|
|
>
|
|
<span className="material-symbols-outlined text-primary-container text-[18px]">
|
|
person
|
|
</span>
|
|
</button>
|
|
|
|
{userMenuOpen && (
|
|
<div
|
|
role="menu"
|
|
aria-orientation="vertical"
|
|
className="absolute right-0 mt-2 w-60 rounded-2xl bg-surface-container-lowest/95 backdrop-blur-2xl border border-brand-border/80 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-brand-border/40 mb-1">
|
|
<span className="font-headline text-xs font-semibold text-white block">
|
|
{club.founderName || 'Rider Account'}
|
|
</span>
|
|
<span className="font-mono text-[10px] text-brand-cyan">
|
|
{permissions.role === 'owner' ? 'Club Owner' : permissions.role === 'admin' ? 'Club Admin' : 'Club Member'}
|
|
</span>
|
|
</div>
|
|
|
|
<Link
|
|
href="/account"
|
|
onClick={() => setUserMenuOpen(false)}
|
|
role="menuitem"
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-slate-200 hover:text-brand-cyan hover:bg-surface-container transition-colors text-xs font-semibold"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px] text-brand-cyan">
|
|
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-slate-200 hover:text-brand-cyan hover:bg-surface-container transition-colors text-xs font-semibold"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px] text-brand-cyan">
|
|
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-slate-200 hover:text-brand-cyan hover:bg-surface-container transition-colors text-xs font-semibold"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px] text-brand-cyan">
|
|
shield
|
|
</span>
|
|
<span>Founder Portal</span>
|
|
</Link>
|
|
|
|
{(permissions.role === 'owner' || permissions.role === 'admin') && (
|
|
<Link
|
|
href="/admin"
|
|
onClick={() => setUserMenuOpen(false)}
|
|
role="menuitem"
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-slate-200 hover:text-brand-cyan hover:bg-surface-container transition-colors text-xs font-semibold"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px] text-brand-cyan">
|
|
admin_panel_settings
|
|
</span>
|
|
<span>Operations Admin</span>
|
|
</Link>
|
|
)}
|
|
|
|
<div className="border-t border-brand-border/40 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>
|
|
</header>
|
|
);
|
|
}
|