Improve sidebar: collapsible, grouped sections, scrollable nav, pinned footer
- Sidebar collapses to icon-only mode with toggle button - Nav items grouped into sections (Customers, Repairs, Storage, Admin) - Each section is independently collapsible - Middle nav area scrolls when items overflow - Help, profile, and sign out pinned to bottom
This commit is contained in:
@@ -8,7 +8,7 @@ import { myPermissionsOptions } from '@/api/rbac'
|
||||
import { moduleListOptions } from '@/api/modules'
|
||||
import { Avatar } from '@/components/shared/avatar-upload'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, KeyRound, Settings } from 'lucide-react'
|
||||
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, KeyRound, Settings, PanelLeftClose, PanelLeft } from 'lucide-react'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated')({
|
||||
beforeLoad: () => {
|
||||
@@ -60,23 +60,50 @@ function StoreLogo() {
|
||||
return <img src={src} alt={storeData?.name ?? 'Store'} className="max-h-10 max-w-[180px] object-contain" />
|
||||
}
|
||||
|
||||
return <h2 className="text-lg font-semibold text-sidebar-foreground">{storeData?.name ?? 'Forte'}</h2>
|
||||
return <h2 className="text-lg font-semibold text-sidebar-foreground">{storeData?.name ?? 'LunarFront'}</h2>
|
||||
}
|
||||
|
||||
function NavLink({ to, icon, label }: { to: string; icon: React.ReactNode; label: string }) {
|
||||
function NavLink({ to, icon, label, collapsed }: { to: string; icon: React.ReactNode; label: string; collapsed?: boolean }) {
|
||||
return (
|
||||
<Link
|
||||
to={to as '/accounts'}
|
||||
search={{} as any}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
title={collapsed ? label : undefined}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
{!collapsed && label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function NavGroup({ label, children, collapsed }: { label: string; children: React.ReactNode; collapsed?: boolean }) {
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
if (collapsed) return <div className="space-y-1">{children}</div>
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center justify-between w-full px-3 mb-1 mt-3 group cursor-pointer"
|
||||
>
|
||||
<span className="text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wide group-hover:text-sidebar-foreground/70">{label}</span>
|
||||
<svg
|
||||
className={`h-3 w-3 text-sidebar-foreground/40 transition-transform ${open ? '' : '-rotate-90'}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && <div className="space-y-1">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AuthenticatedLayout() {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore((s) => s.user)
|
||||
@@ -117,76 +144,92 @@ function AuthenticatedLayout() {
|
||||
const canViewRepairs = !permissionsLoaded || hasPermission('repairs.view')
|
||||
const canViewUsers = !permissionsLoaded || hasPermission('users.view')
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="flex">
|
||||
<nav className="w-56 border-r border-border bg-sidebar min-h-screen flex flex-col">
|
||||
<div className="p-4 space-y-1">
|
||||
<StoreLogo />
|
||||
<p className="text-[10px] text-sidebar-foreground/40 tracking-wide">Amplified by <span className="font-semibold text-sidebar-foreground/60">Forte</span></p>
|
||||
</div>
|
||||
|
||||
{/* Sidebar links use `as any` on search because TanStack Router
|
||||
requires the full validated search shape, but these links just
|
||||
navigate to the page with default params. */}
|
||||
<div className="flex-1 px-3 space-y-1">
|
||||
{canViewAccounts && (
|
||||
<>
|
||||
<NavLink to="/accounts" icon={<Users className="h-4 w-4" />} label="Accounts" />
|
||||
<NavLink to="/members" icon={<UserRound className="h-4 w-4" />} label="Members" />
|
||||
</>
|
||||
)}
|
||||
{isModuleEnabled('repairs') && canViewRepairs && (
|
||||
<>
|
||||
<NavLink to="/repairs" icon={<Wrench className="h-4 w-4" />} label="Repairs" />
|
||||
<NavLink to="/repair-batches" icon={<Package className="h-4 w-4" />} label="Repair Batches" />
|
||||
{hasPermission('repairs.admin') && (
|
||||
<NavLink to="/repairs/templates" icon={<ClipboardList className="h-4 w-4" />} label="Repair Templates" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isModuleEnabled('files') && (
|
||||
<NavLink to="/files" icon={<FolderOpen className="h-4 w-4" />} label="Files" />
|
||||
)}
|
||||
{isModuleEnabled('vault') && (
|
||||
<NavLink to="/vault" icon={<KeyRound className="h-4 w-4" />} label="Vault" />
|
||||
)}
|
||||
{canViewUsers && (
|
||||
<div className="mt-4 mb-1 px-3">
|
||||
<span className="text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wide">Admin</span>
|
||||
<nav className={`${collapsed ? 'w-14' : 'w-56'} border-r border-border bg-sidebar h-screen flex flex-col sticky top-0 transition-[width] duration-200`}>
|
||||
{/* Header — logo & collapse toggle */}
|
||||
<div className={`p-3 flex items-center ${collapsed ? 'justify-center' : 'justify-between'}`}>
|
||||
{!collapsed && (
|
||||
<div className="min-w-0">
|
||||
<StoreLogo />
|
||||
<p className="text-[10px] text-sidebar-foreground/40 tracking-wide">Powered by <span className="font-semibold text-sidebar-foreground/60">LunarFront</span></p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0 text-sidebar-foreground/50 hover:text-sidebar-foreground"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{collapsed ? <PanelLeft className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable nav links */}
|
||||
<div className="flex-1 overflow-y-auto px-2 space-y-1 scrollbar-thin">
|
||||
{canViewAccounts && (
|
||||
<NavGroup label="Customers" collapsed={collapsed}>
|
||||
<NavLink to="/accounts" icon={<Users className="h-4 w-4" />} label="Accounts" collapsed={collapsed} />
|
||||
<NavLink to="/members" icon={<UserRound className="h-4 w-4" />} label="Members" collapsed={collapsed} />
|
||||
</NavGroup>
|
||||
)}
|
||||
{isModuleEnabled('repairs') && canViewRepairs && (
|
||||
<NavGroup label="Repairs" collapsed={collapsed}>
|
||||
<NavLink to="/repairs" icon={<Wrench className="h-4 w-4" />} label="Tickets" collapsed={collapsed} />
|
||||
<NavLink to="/repair-batches" icon={<Package className="h-4 w-4" />} label="Batches" collapsed={collapsed} />
|
||||
{hasPermission('repairs.admin') && (
|
||||
<NavLink to="/repairs/templates" icon={<ClipboardList className="h-4 w-4" />} label="Templates" collapsed={collapsed} />
|
||||
)}
|
||||
</NavGroup>
|
||||
)}
|
||||
{(isModuleEnabled('files') || isModuleEnabled('vault')) && (
|
||||
<NavGroup label="Storage" collapsed={collapsed}>
|
||||
{isModuleEnabled('files') && (
|
||||
<NavLink to="/files" icon={<FolderOpen className="h-4 w-4" />} label="Files" collapsed={collapsed} />
|
||||
)}
|
||||
{isModuleEnabled('vault') && (
|
||||
<NavLink to="/vault" icon={<KeyRound className="h-4 w-4" />} label="Vault" collapsed={collapsed} />
|
||||
)}
|
||||
</NavGroup>
|
||||
)}
|
||||
{canViewUsers && (
|
||||
<>
|
||||
<NavLink to="/users" icon={<UserCog className="h-4 w-4" />} label="Users" />
|
||||
<NavLink to="/roles" icon={<Shield className="h-4 w-4" />} label="Roles" />
|
||||
<NavLink to="/settings" icon={<Settings className="h-4 w-4" />} label="Settings" />
|
||||
</>
|
||||
<NavGroup label="Admin" collapsed={collapsed}>
|
||||
<NavLink to="/users" icon={<UserCog className="h-4 w-4" />} label="Users" collapsed={collapsed} />
|
||||
<NavLink to="/roles" icon={<Shield className="h-4 w-4" />} label="Roles" collapsed={collapsed} />
|
||||
<NavLink to="/settings" icon={<Settings className="h-4 w-4" />} label="Settings" collapsed={collapsed} />
|
||||
</NavGroup>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t border-sidebar-border space-y-1">
|
||||
<NavLink to="/help" icon={<HelpCircle className="h-4 w-4" />} label="Help" />
|
||||
{/* Pinned footer — help, profile, sign out */}
|
||||
<div className="shrink-0 p-2 border-t border-sidebar-border space-y-1">
|
||||
<NavLink to="/help" icon={<HelpCircle className="h-4 w-4" />} label="Help" collapsed={collapsed} />
|
||||
<Link
|
||||
to="/profile"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent w-full"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground w-full' }}
|
||||
title={collapsed ? `${user?.firstName} ${user?.lastName}` : undefined}
|
||||
>
|
||||
{user?.id ? <Avatar entityType="user" entityId={user.id} size="sm" /> : <User className="h-4 w-4" />}
|
||||
<span className="truncate">{user?.firstName} {user?.lastName}</span>
|
||||
{!collapsed && <span className="truncate">{user?.firstName} {user?.lastName}</span>}
|
||||
</Link>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2 text-sm text-sidebar-foreground/70 hover:text-sidebar-foreground"
|
||||
className={`w-full justify-start gap-2 text-sm text-sidebar-foreground/70 hover:text-sidebar-foreground ${collapsed ? 'px-3' : ''}`}
|
||||
onClick={handleLogout}
|
||||
title={collapsed ? 'Sign out' : undefined}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Sign out
|
||||
{!collapsed && 'Sign out'}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1 p-6">
|
||||
<main className="flex-1 p-6 min-h-screen">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user