325 lines
12 KiB
TypeScript
325 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', disabled: false },
|
|
{ id: 'wall', label: 'Wall', icon: 'forum', disabled: false },
|
|
{ id: 'chat', label: 'Chat', icon: 'chat', disabled: false },
|
|
{ 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 === 'routes') return `/clubs/${clubParam}/manage/routes${queryParam}`;
|
|
if (id === 'wall') return `/clubs/${clubParam}/manage/wall${queryParam}`;
|
|
if (id === 'chat') return `/clubs/${clubParam}/manage/chat${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>
|
|
</>
|
|
);
|
|
}
|