feat: add permission-gated navigation and admin access
This commit is contained in:
18
src/components/AppLoader.tsx
Normal file
18
src/components/AppLoader.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
interface AppLoaderProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function AppLoader({ label: _label }: AppLoaderProps) {
|
||||
return (
|
||||
<main
|
||||
className="flex min-h-screen items-center justify-center bg-background"
|
||||
aria-label="Loading"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2.5 animate-bounce rounded-full bg-primary [animation-delay:-0.24s]" />
|
||||
<span className="size-2.5 animate-bounce rounded-full bg-primary [animation-delay:-0.12s]" />
|
||||
<span className="size-2.5 animate-bounce rounded-full bg-primary" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import { LayoutDashboard, Plus, Layers, Package, Milestone, ShieldCheck, Users } from 'lucide-react';
|
||||
import { ChevronRight, Package } from 'lucide-react';
|
||||
import { NavUser } from '@/components/nav-user';
|
||||
import { ROUTES } from '@/utils/routes';
|
||||
import { filterMenuItems, menuItems, type MenuItem } from '@/config/menu.config';
|
||||
import { usePermissions } from '@/hooks/usePermissions';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -12,6 +14,9 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
} from '@/components/ui/sidebar';
|
||||
@@ -19,50 +24,11 @@ import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useAppStore } from '@/store/app.store';
|
||||
|
||||
// This is the navigation data
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: ROUTES.DASHBOARD,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: 'New Analysis',
|
||||
url: ROUTES.UPLOAD,
|
||||
icon: Plus,
|
||||
},
|
||||
{
|
||||
title: 'Project',
|
||||
url: ROUTES.PROJECT,
|
||||
icon: Layers,
|
||||
},
|
||||
{
|
||||
title: 'Package',
|
||||
url: ROUTES.PACKAGE,
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: 'Segment',
|
||||
url: ROUTES.CHAINAGE,
|
||||
icon: Milestone,
|
||||
},
|
||||
{
|
||||
title: 'Roles',
|
||||
url: ROUTES.ROLES,
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
url: ROUTES.USERS,
|
||||
icon: Users,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const pathname = usePathname();
|
||||
const { hasPermission } = usePermissions();
|
||||
const authUser = useAppStore((state) => state.user);
|
||||
const navItems = filterMenuItems(menuItems, hasPermission);
|
||||
const user = {
|
||||
name: [authUser?.first_name, authUser?.last_name].filter(Boolean).join(' ') || 'Admin',
|
||||
email: authUser?.email || '',
|
||||
@@ -85,15 +51,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
{data.navMain.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild tooltip={item.title} isActive={pathname === item.url}>
|
||||
<Link href={item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
{navItems.map((item) => (
|
||||
<SidebarNavItem key={item.title} item={item} pathname={pathname} />
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
@@ -105,3 +64,59 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarNavItem({ item, pathname }: { item: MenuItem; pathname: string }) {
|
||||
const isActive = item.path ? pathname === item.path : false;
|
||||
const isChildActive = item.children?.some((child) => child.path === pathname) ?? false;
|
||||
const hasChildren = Boolean(item.children?.length);
|
||||
|
||||
if (hasChildren) {
|
||||
return (
|
||||
<Collapsible asChild defaultOpen={isChildActive} className="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title} isActive={isChildActive}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.children?.map((child) => (
|
||||
<SidebarMenuSubItem key={child.title}>
|
||||
{child.path ? (
|
||||
<SidebarMenuSubButton asChild isActive={pathname === child.path}>
|
||||
<Link href={child.path}>
|
||||
<child.icon />
|
||||
<span>{child.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
) : null}
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
{item.path ? (
|
||||
<SidebarMenuButton asChild tooltip={item.title} isActive={isActive}>
|
||||
<Link href={item.path}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
) : (
|
||||
<SidebarMenuButton tooltip={item.title} isActive={isChildActive}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user