MotorSignal-App/website/components/founder-portal/founder-sidebar.tsx

323 lines
12 KiB
TypeScript

'use client';
import React from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { Club, UserPortalPermissions } from './types';
interface FounderSidebarProps {
club: Club;
permissions: UserPortalPermissions;
activeNav?: string;
onNavSelect?: (nav: string) => void;
pendingApplicationsCount?: number;
upcomingRidesCount?: number;
isOpenOnMobile?: boolean;
onCloseMobile?: () => void;
}
export function FounderSidebar({
club,
permissions,
activeNav = 'overview',
onNavSelect,
pendingApplicationsCount = 0,
upcomingRidesCount = 0,
isOpenOnMobile = false,
onCloseMobile,
}: FounderSidebarProps) {
const searchParams = useSearchParams();
const isDemo = searchParams?.get('preview') === 'demo';
const queryParam = isDemo ? '?preview=demo' : '';
const clubParam = club.slug || club.id;
const shortCode = club.name
.split(/\s+/)
.filter(Boolean)
.map((w) => w[0].toUpperCase())
.slice(0, 3)
.join('') || 'ATL';
const navItemsTop = [
{ id: 'overview', label: 'Overview', icon: 'dashboard', badge: 'ACTIVE', badgeType: 'primary', disabled: false },
{ id: 'profile', label: 'Club Profile', icon: 'shield', disabled: false },
{ id: 'members', label: 'Members', icon: 'group', disabled: false },
{
id: 'applications',
label: 'Applications',
icon: 'person_add',
badge: pendingApplicationsCount > 0 ? `${pendingApplicationsCount} PENDING` : undefined,
badgeType: 'error',
disabled: false,
},
{ id: 'ranks_roles', label: 'Ranks & Roles', icon: 'military_tech', disabled: false },
];
const navItemsMiddle = [
{
id: 'rides',
label: 'Rides',
icon: 'route',
badge: upcomingRidesCount > 0 ? `${upcomingRidesCount}` : undefined,
badgeType: 'secondary',
disabled: false,
},
{ id: 'routes', label: 'Routes', icon: 'map', badge: 'SOON', badgeType: 'disabled', disabled: true },
{ id: 'wall', label: 'Wall', icon: 'forum', disabled: false },
{ id: 'chat', label: 'Chat', icon: 'chat', badge: 'SOON', badgeType: 'disabled', disabled: true },
{ id: 'stats', label: 'Stats', icon: 'query_stats', disabled: false },
];
const navItemsBottom = [
{ id: 'moderation', label: 'Moderation', icon: 'gavel', disabled: false },
{ id: 'settings', label: 'Settings', icon: 'settings', disabled: false },
];
function getRouteHref(id: string) {
if (id === 'overview') return `/clubs/${clubParam}/manage${queryParam}`;
if (id === 'profile') return `/clubs/${clubParam}/manage/profile${queryParam}`;
if (id === 'members') return `/clubs/${clubParam}/manage/members${queryParam}`;
if (id === 'wall') return `/clubs/${clubParam}/manage/wall${queryParam}`;
if (id === 'stats') return `/clubs/${clubParam}/manage/stats${queryParam}`;
if (id === 'moderation') return `/clubs/${clubParam}/manage/moderation${queryParam}`;
if (id === 'applications') return `/clubs/${clubParam}/manage/applications${queryParam}`;
if (id === 'ranks_roles') return `/clubs/${clubParam}/manage/ranks-roles${queryParam}`;
if (id === 'rides') return `/clubs/${clubParam}/manage/rides${queryParam}`;
if (id === 'settings') return `/clubs/${clubParam}/manage/settings${queryParam}`;
return undefined;
}
function handleItemClick(id: string) {
onNavSelect?.(id);
onCloseMobile?.();
}
function renderNavItem(item: {
id: string;
label: string;
icon: string;
badge?: string;
badgeType?: string;
disabled?: boolean;
}) {
const isActive = activeNav === item.id;
const href = !item.disabled ? getRouteHref(item.id) : undefined;
const isProfile = item.id === 'profile';
const isSettings = item.id === 'settings';
const isApplications = item.id === 'applications';
const isRides = item.id === 'rides';
const isSubRoute = isProfile || isSettings || isApplications || isRides || item.id === 'ranks_roles' || item.id === 'members' || item.id === 'wall' || item.id === 'stats' || item.id === 'moderation';
if (item.disabled) {
return (
<div
key={item.id}
className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs text-slate-500/60 cursor-not-allowed select-none"
title={`${item.label} is planned for a future release`}
aria-disabled="true"
>
<div className="flex items-center gap-2.5">
<span className="material-symbols-outlined text-[18px] text-slate-600">
{item.icon}
</span>
<span className="text-slate-500">{item.label}</span>
</div>
{item.badge && (
<span className="font-mono text-[9px] px-1.5 py-0.5 rounded bg-surface-container text-slate-500 border border-brand-border/40">
{item.badge}
</span>
)}
</div>
);
}
const content = (
<>
<div className="flex items-center gap-2.5">
<span
className={`material-symbols-outlined text-[18px] ${
isActive && isSubRoute ? 'text-brand-cyan' : ''
}`}
style={isActive && isSubRoute ? { fontVariationSettings: "'FILL' 1" } : undefined}
>
{item.icon}
</span>
<span className={isActive && isSubRoute ? 'text-brand-cyan font-semibold' : ''}>
{item.label}
</span>
</div>
{item.badge && (
<span
className={`font-mono text-[10px] px-1.5 py-0.5 rounded ${
item.badgeType === 'primary' && isActive
? 'bg-primary-container/20 text-brand-cyan font-medium border border-brand-cyan/30'
: item.badgeType === 'error'
? 'bg-error-container text-on-error-container font-semibold border border-error/30'
: item.badgeType === 'secondary'
? 'bg-brand-cyan/10 text-brand-cyan border border-brand-cyan/30 font-semibold'
: 'bg-surface-container-high text-on-surface-variant border border-outline-variant'
}`}
>
{item.badge}
</span>
)}
{isActive && isSubRoute && !item.badge && (
<span className="w-1.5 h-1.5 rounded-full bg-brand-cyan" />
)}
</>
);
const baseClasses = `w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors text-xs ${
isActive
? 'bg-surface-container-high text-primary-container border-l-2 border-primary-container font-medium'
: 'text-on-surface-variant hover:bg-surface-container hover:text-on-surface font-normal'
}`;
if (href) {
return (
<Link
key={item.id}
href={href}
onClick={() => {
onNavSelect?.(item.id);
onCloseMobile?.();
}}
className={baseClasses}
>
{content}
</Link>
);
}
return (
<button
key={item.id}
onClick={() => handleItemClick(item.id)}
className={baseClasses}
>
{content}
</button>
);
}
return (
<>
{/* Mobile Backdrop */}
{isOpenOnMobile && (
<div
onClick={onCloseMobile}
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-40 lg:hidden"
aria-hidden="true"
/>
)}
<aside
className={`fixed left-0 top-0 h-full w-72 bg-surface-container-lowest z-50 flex flex-col justify-between border-r border-outline-variant transition-transform duration-200 ease-in-out ${
isOpenOnMobile ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
}`}
aria-label="Founder Portal Sidebar"
>
<div className="flex flex-col flex-1 min-h-0">
{/* Brand Header */}
<div className="h-16 px-5 flex items-center justify-between border-b border-outline-variant bg-surface-container-lowest">
<Link href="/" className="flex items-center gap-3 group">
<div className="w-8 h-8 rounded bg-surface-container-high border border-outline-variant flex items-center justify-center text-primary group-hover:border-primary/50 transition-colors">
<span className="material-symbols-outlined text-[20px]">two_wheeler</span>
</div>
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className="font-headline font-semibold text-[15px] tracking-tight text-on-surface">
MotorSignal
</span>
<span className="font-mono text-[10px] text-outline px-1.5 py-0.5 rounded bg-surface-container-high border border-outline-variant">
Portal
</span>
</div>
<span className="font-mono text-[10px] text-brand-cyan tracking-normal font-medium">
Club Management
</span>
</div>
</Link>
{/* Mobile close button */}
<button
onClick={onCloseMobile}
className="lg:hidden text-outline hover:text-on-surface p-1"
aria-label="Close sidebar"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
{/* Current Club Context */}
<div className="px-5 py-3 border-b border-outline-variant bg-surface-container-low/60">
<div className="flex items-center justify-between">
<span className="font-mono text-[10px] text-outline tracking-normal font-medium">
Current Club
</span>
<span className="flex items-center gap-1.5 font-mono text-[10px] text-brand-cyan">
<span className="w-1.5 h-1.5 rounded-full bg-brand-cyan" />
Online / Synced
</span>
</div>
<div className="mt-1 flex items-center justify-between font-body text-xs text-on-surface">
<span className="truncate font-semibold">{club.name}</span>
<span className="font-mono text-[11px] text-outline ml-2 shrink-0">{shortCode}</span>
</div>
</div>
{/* Navigation Links */}
<nav className="flex-1 overflow-y-auto px-3 py-3 space-y-1">
{navItemsTop.map(renderNavItem)}
{/* Divider */}
<div className="pt-2 pb-1 px-3">
<div className="h-px bg-outline-variant/60 w-full" />
</div>
{navItemsMiddle.map(renderNavItem)}
{/* Divider */}
<div className="pt-2 pb-1 px-3">
<div className="h-px bg-outline-variant/60 w-full" />
</div>
{navItemsBottom.map(renderNavItem)}
</nav>
</div>
{/* Bottom Founder Profile */}
<div className="p-3 border-t border-brand-border/60 bg-surface-container-low">
<div className="flex items-center justify-between p-2 rounded-xl bg-surface-container border border-brand-border/60">
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-8 h-8 rounded-lg bg-gradient-to-tr from-brand-cardAlt to-slate-800 border border-brand-border/80 flex items-center justify-center text-brand-cyan shrink-0">
<span className="material-symbols-outlined text-[18px]">person</span>
</div>
<div className="flex flex-col min-w-0">
<div className="flex items-center gap-1">
<span className="font-body text-xs font-semibold text-on-surface truncate">
{club.founderName ?? 'Club Founder'}
</span>
<span className="material-symbols-outlined text-[13px] text-brand-cyan shrink-0">
verified
</span>
</div>
<span className="font-mono text-[10px] text-outline truncate">
{permissions.role === 'owner' ? 'Owner & President' : permissions.role.toUpperCase()}
</span>
</div>
</div>
<Link
href="/account"
className="text-outline hover:text-on-surface transition-colors p-1"
title="Account Settings"
>
<span className="material-symbols-outlined text-[18px]">settings</span>
</Link>
</div>
</div>
</aside>
</>
);
}