372 lines
10 KiB
TypeScript
372 lines
10 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import {
|
|
Home,
|
|
FileText,
|
|
GraduationCap,
|
|
Users,
|
|
Calendar,
|
|
Globe,
|
|
BarChart3,
|
|
Settings,
|
|
Menu,
|
|
ChevronLeft,
|
|
LogOut,
|
|
User,
|
|
Shield,
|
|
Building,
|
|
UserPlus,
|
|
Webcam,
|
|
ClipboardList,
|
|
Target
|
|
} from 'lucide-react';
|
|
import {
|
|
Breadcrumb,
|
|
BreadcrumbItem,
|
|
BreadcrumbLink,
|
|
BreadcrumbList,
|
|
BreadcrumbPage,
|
|
BreadcrumbSeparator,
|
|
} from '../ui/breadcrumb';
|
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import klcLogoDark from '../../assets/klc-logo-white.png';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
interface NavigationItem {
|
|
id: string;
|
|
label: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
route: string;
|
|
children?: NavigationItem[];
|
|
}
|
|
|
|
interface AuthenticatedLayoutProps {
|
|
children: React.ReactNode;
|
|
currentRoute: string;
|
|
// onNavigate: (route: string) => void;
|
|
onLogout: () => void;
|
|
user: {
|
|
name: string;
|
|
email: string;
|
|
role: string;
|
|
avatar?: string;
|
|
lastLogin: string;
|
|
};
|
|
breadcrumbs?: Array<{ label: string; route?: string }>;
|
|
}
|
|
|
|
const navigationItems: NavigationItem[] = [
|
|
{
|
|
id: 'dashboard',
|
|
label: 'Dashboard',
|
|
icon: Home,
|
|
route: '/dashboard'
|
|
},
|
|
{
|
|
id: 'content',
|
|
label: 'Content',
|
|
icon: FileText,
|
|
route: '/content'
|
|
},
|
|
{
|
|
id: 'courses',
|
|
label: 'Courses',
|
|
icon: GraduationCap,
|
|
route: '/courses'
|
|
},
|
|
{
|
|
id: 'profilers',
|
|
label: 'Profilers',
|
|
icon: Target,
|
|
route: '/profilers'
|
|
},
|
|
{
|
|
id: 'programmes',
|
|
label: 'Programmes',
|
|
icon: ClipboardList,
|
|
route: '/programmes'
|
|
},
|
|
{
|
|
id: 'open-programme',
|
|
label: 'Open Programme',
|
|
icon: Globe,
|
|
route: '/open-programme'
|
|
},
|
|
{
|
|
id: 'webinars',
|
|
label: 'Webinars',
|
|
icon: Webcam,
|
|
route: '/webinars'
|
|
},
|
|
{
|
|
id: 'class-scheduler',
|
|
label: 'Class Scheduler',
|
|
icon: Calendar,
|
|
route: '/class-scheduler'
|
|
},
|
|
{
|
|
id: 'landing-pages',
|
|
label: 'Landing Pages',
|
|
icon: Globe,
|
|
route: '/landing-pages'
|
|
},
|
|
{
|
|
id: 'users',
|
|
label: 'Users',
|
|
icon: Users,
|
|
route: '/users/individual',
|
|
children: [
|
|
{
|
|
id: 'individual',
|
|
label: 'Individual Learners',
|
|
icon: User,
|
|
route: '/users/individual'
|
|
},
|
|
{
|
|
id: 'organizations',
|
|
label: 'Organizations',
|
|
icon: Building,
|
|
route: '/users/organizations'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'roles',
|
|
label: 'Roles',
|
|
icon: Shield,
|
|
route: '/admin/roles'
|
|
},
|
|
{
|
|
id: 'analytics',
|
|
label: 'Analytics',
|
|
icon: BarChart3,
|
|
route: '/admin/analytics'
|
|
},
|
|
{
|
|
id: 'settings',
|
|
label: 'Settings',
|
|
icon: Settings,
|
|
route: '/settings',
|
|
children: [
|
|
{
|
|
id: 'leads',
|
|
label: 'Leads',
|
|
icon: UserPlus,
|
|
route: '/admin/leads'
|
|
},
|
|
{
|
|
id: 'facilities',
|
|
label: 'Facilities',
|
|
icon: Building,
|
|
route: '/admin/facilities'
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
export function AuthenticatedLayout({
|
|
children,
|
|
currentRoute,
|
|
// onNavigate,
|
|
onLogout,
|
|
user,
|
|
breadcrumbs = []
|
|
}: AuthenticatedLayoutProps) {
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
|
|
const isActiveRoute = (route: string) => {
|
|
return currentRoute === route ||
|
|
(route === '/users/individual' && currentRoute.startsWith('/users/'));
|
|
};
|
|
|
|
const getActiveParent = (items: NavigationItem[]): string | null => {
|
|
for (const item of items) {
|
|
if (isActiveRoute(item.route)) return item.id;
|
|
if (item.children) {
|
|
for (const child of item.children) {
|
|
if (isActiveRoute(child.route)) return item.id;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const activeParent = getActiveParent(navigationItems);
|
|
|
|
const renderNavigationItem = (item: NavigationItem, isChild = false) => {
|
|
const isActive = isActiveRoute(item.route);
|
|
const isParentActive = activeParent === item.id;
|
|
const Icon = item.icon;
|
|
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<div key={item.id}>
|
|
<Button
|
|
variant="ghost"
|
|
className={`w-full justify-start min-h-[44px] ${
|
|
isActive
|
|
? 'bg-primary text-primary-foreground'
|
|
: isParentActive && !isChild
|
|
? 'bg-accent text-accent-foreground'
|
|
: 'hover:bg-accent hover:text-accent-foreground'
|
|
} ${isChild ? 'ml-4 text-sm' : ''}`}
|
|
onClick={() => navigate(item.route)}
|
|
>
|
|
<Icon className={`${sidebarCollapsed ? 'mx-auto' : 'mr-2'} h-4 w-4 flex-shrink-0`} />
|
|
{!sidebarCollapsed && (
|
|
<span className="truncate">{item.label}</span>
|
|
)}
|
|
</Button>
|
|
|
|
{/* Render children if parent is active and not collapsed */}
|
|
{item.children && isParentActive && !sidebarCollapsed && (
|
|
<div className="mt-1 space-y-1">
|
|
{item.children.map(child => renderNavigationItem(child, true))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
const navigate = useNavigate();
|
|
return (
|
|
<div className="flex h-screen bg-background">
|
|
{/* Left Sidebar */}
|
|
<div
|
|
className={`${sidebarCollapsed ? 'w-[72px]' : 'w-[240px]'} bg-sidebar border-r border-sidebar-border flex flex-col transition-all duration-200`}
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
>
|
|
{/* Logo Header */}
|
|
<div className="p-4 border-b border-sidebar-border" style={{ backgroundColor: 'var(--color-brand-primary)' }}>
|
|
<div className="flex items-center justify-between">
|
|
{!sidebarCollapsed && (
|
|
<img
|
|
src={klcLogoDark}
|
|
alt="Kautilya Leadership Centre"
|
|
className="h-8"
|
|
/>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
className="text-white hover:bg-white/10 min-h-[44px] min-w-[44px]"
|
|
aria-label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
>
|
|
{sidebarCollapsed ? <Menu className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation Items */}
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
<nav className="space-y-2" role="navigation">
|
|
{navigationItems.map(item => renderNavigationItem(item))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* User Section */}
|
|
<div className="p-4 border-t border-sidebar-border"
|
|
onClick={() => navigate('/profile')}
|
|
>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className={`w-full ${sidebarCollapsed ? 'px-2' : 'justify-start'} min-h-[44px]`}
|
|
>
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback style={{ backgroundColor: 'var(--color-brand-accent)', color: 'var(--color-brand-primary)' }}>
|
|
{user.name.split(' ').map(n => n[0]).join('')}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
{!sidebarCollapsed && (
|
|
<div className="ml-2 text-left">
|
|
<div className="font-medium truncate">{user.name}</div>
|
|
<div className="text-xs text-muted-foreground">{user.role}</div>
|
|
</div>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<div className="px-2 py-1.5 text-sm">
|
|
<div className="font-medium">{user.name}</div>
|
|
<div className="text-muted-foreground">{user.email}</div>
|
|
</div>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => navigate('/profile')}>
|
|
<User className="mr-2 h-4 w-4" />
|
|
Profile
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={onLogout} className="text-destructive">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
Sign out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* Breadcrumb */}
|
|
{breadcrumbs.length > 0 && (
|
|
<div className="border-b bg-muted/30 px-6 py-3">
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink
|
|
onClick={() => navigate('/dashboard')}
|
|
className="cursor-pointer"
|
|
>
|
|
Admin
|
|
</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
{breadcrumbs.map((crumb, index) => (
|
|
<React.Fragment key={index}>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
{crumb.route && index < breadcrumbs.length - 1 ? (
|
|
<BreadcrumbLink
|
|
onClick={() => navigate(crumb.route!)}
|
|
className="cursor-pointer"
|
|
>
|
|
{crumb.label}
|
|
</BreadcrumbLink>
|
|
) : (
|
|
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
|
)}
|
|
</BreadcrumbItem>
|
|
</React.Fragment>
|
|
))}
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
</div>
|
|
)}
|
|
|
|
{/* Page Content */}
|
|
<main className="flex-1 overflow-y-auto">
|
|
{children}
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="border-t bg-muted/30 px-6 py-3">
|
|
<div className="flex justify-center space-x-6 text-sm text-muted-foreground">
|
|
<span>© 2024 Kautilya Leadership Centre</span>
|
|
<button className="hover:text-foreground transition-colors">Privacy Policy</button>
|
|
<button className="hover:text-foreground transition-colors">Terms of Service</button>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |