531 lines
19 KiB
TypeScript
531 lines
19 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
|
import { Badge } from '../ui/badge';
|
|
import { Sheet, SheetContent, SheetTrigger } from '../ui/sheet';
|
|
import { ScrollArea } from '../ui/scroll-area';
|
|
import { Separator } from '../ui/separator';
|
|
// import logo from "../../assets/klc-logo.png"
|
|
const logo = new URL("../../assets/klc-logo.png", import.meta.url).href
|
|
import {
|
|
Menu,
|
|
Search,
|
|
Bell,
|
|
ChevronDown,
|
|
Home,
|
|
BookOpen,
|
|
User,
|
|
BarChart3,
|
|
MessageSquare,
|
|
Calendar,
|
|
Trophy,
|
|
Building2,
|
|
Users,
|
|
X,
|
|
Settings
|
|
} from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
interface LearnerLayoutProps {
|
|
children: React.ReactNode;
|
|
currentPage?: string;
|
|
userType?: 'individual' | 'corporate';
|
|
user?: {
|
|
name: string;
|
|
email: string;
|
|
avatar?: string;
|
|
organization?: string;
|
|
orgLogo?: string;
|
|
role?: string;
|
|
cohort?: string;
|
|
};
|
|
}
|
|
|
|
interface NotificationPanelProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
notifications: Array<{
|
|
id: string;
|
|
type: 'info' | 'warning' | 'success' | 'error';
|
|
title: string;
|
|
message: string;
|
|
time: string;
|
|
read: boolean;
|
|
}>;
|
|
}
|
|
|
|
function NotificationPanel({ isOpen, onClose, notifications }: NotificationPanelProps) {
|
|
const unreadCount = notifications.filter(n => !n.read).length;
|
|
const navigate = useNavigate();
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 lg:relative lg:inset-auto">
|
|
{/* Mobile overlay */}
|
|
<div className="lg:hidden fixed inset-0 bg-black/50" onClick={onClose} />
|
|
|
|
{/* Panel */}
|
|
<div className="fixed right-0 top-0 h-full w-80 bg-card border-l border-border shadow-xl lg:absolute lg:top-full lg:right-0 lg:h-auto lg:max-h-96 lg:rounded-lg lg:border lg:shadow-lg">
|
|
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
<h3 className="font-semibold text-lg">
|
|
Notifications {unreadCount > 0 && <Badge variant="secondary" className="ml-2">{unreadCount}</Badge>}
|
|
</h3>
|
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<ScrollArea className="h-80 lg:h-64">
|
|
<div className="p-4 space-y-3">
|
|
{notifications.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<Bell className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>No notifications</p>
|
|
</div>
|
|
) : (
|
|
notifications.map((notification) => (
|
|
<div
|
|
key={notification.id}
|
|
className={`p-3 rounded-lg border ${notification.read ? 'bg-muted/50' : 'bg-background'} hover:bg-muted/70 transition-colors cursor-pointer`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className={`w-2 h-2 rounded-full mt-2 flex-shrink-0 ${notification.type === 'info' ? 'bg-blue-500' :
|
|
notification.type === 'success' ? 'bg-success' :
|
|
notification.type === 'warning' ? 'bg-yellow-500' :
|
|
'bg-destructive'
|
|
}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-lg text-foreground">{notification.title}</p>
|
|
<p className="text-lg text-muted-foreground mt-1">{notification.message}</p>
|
|
<p className="text-sm text-muted-foreground mt-2">{notification.time}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{notifications.length > 0 && (
|
|
<div className="p-4 border-t border-border">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full text-lg min-h-[44px]"
|
|
onClick={() => navigate('/notifications')}
|
|
>
|
|
View All Notifications
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
export function LearnerLayout({ children, currentPage, userType = 'individual', user }: LearnerLayoutProps) {
|
|
// Get current path if not provided
|
|
const currentPath = currentPage || window.location.pathname;
|
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
const [notificationsOpen, setNotificationsOpen] = useState(false);
|
|
const [searchValue, setSearchValue] = useState('');
|
|
const navigate = useNavigate();
|
|
|
|
|
|
// Get current view parameter directly from URL
|
|
const getCurrentView = () => {
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
return searchParams.get('view');
|
|
};
|
|
|
|
const [currentView, setCurrentView] = useState(getCurrentView);
|
|
|
|
// Update URL params when location changes
|
|
useEffect(() => {
|
|
const handleLocationChange = () => {
|
|
setCurrentView(getCurrentView());
|
|
};
|
|
|
|
window.addEventListener('popstate', handleLocationChange);
|
|
|
|
// Listen for programmatic navigation changes
|
|
const originalPushState = window.history.pushState;
|
|
const originalReplaceState = window.history.replaceState;
|
|
|
|
window.history.pushState = function (...args) {
|
|
originalPushState.apply(window.history, args);
|
|
setTimeout(handleLocationChange, 0);
|
|
};
|
|
|
|
window.history.replaceState = function (...args) {
|
|
originalReplaceState.apply(window.history, args);
|
|
setTimeout(handleLocationChange, 0);
|
|
};
|
|
|
|
return () => {
|
|
window.removeEventListener('popstate', handleLocationChange);
|
|
window.history.pushState = originalPushState;
|
|
window.history.replaceState = originalReplaceState;
|
|
};
|
|
}, []);
|
|
|
|
// Mock notifications
|
|
const notifications = [
|
|
{
|
|
id: '1',
|
|
type: 'info' as const,
|
|
title: 'New Course Available',
|
|
message: 'Strategic Leadership Foundations is now available in your library',
|
|
time: '2 hours ago',
|
|
read: false
|
|
},
|
|
{
|
|
id: '2',
|
|
type: 'warning' as const,
|
|
title: 'Assignment Due Soon',
|
|
message: 'Leadership Assessment due in 3 days',
|
|
time: '1 day ago',
|
|
read: false
|
|
},
|
|
{
|
|
id: '3',
|
|
type: 'success' as const,
|
|
title: 'Certificate Earned',
|
|
message: 'Congratulations! You completed Management Essentials',
|
|
time: '3 days ago',
|
|
read: true
|
|
}
|
|
];
|
|
|
|
const unreadCount = notifications.filter(n => !n.read).length;
|
|
|
|
// Navigation items with simplified active state logic
|
|
const navigationItems = [
|
|
{
|
|
name: 'Dashboard',
|
|
icon: Home,
|
|
href: `/dashboard?view=${userType}`,
|
|
active: currentPath === '/dashboard' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
},
|
|
{
|
|
name: 'Library',
|
|
icon: BookOpen,
|
|
href: `/library?view=${userType}`,
|
|
active: currentPath === '/library' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
},
|
|
{
|
|
name: 'Surveys',
|
|
icon: MessageSquare,
|
|
href: `/surveys?view=${userType}`,
|
|
active: currentPath === '/surveys' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
},
|
|
{
|
|
name: 'Webinars',
|
|
icon: Calendar,
|
|
href: `/webinars?view=${userType}`,
|
|
active: (currentPath === '/webinars' || currentPath === '/individual-webinars') && (currentView === userType || (!currentView && userType === 'individual'))
|
|
},
|
|
{
|
|
name: 'Leaderboard',
|
|
icon: Trophy,
|
|
href: `/leaderboard?view=${userType}`,
|
|
active: currentPath === '/leaderboard' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
},
|
|
{
|
|
name: 'Settings',
|
|
icon: Settings,
|
|
href: `/settings?view=${userType}`,
|
|
active: currentPath?.startsWith('/settings') && (currentView === userType || (!currentView && userType === 'individual'))
|
|
}
|
|
];
|
|
|
|
|
|
|
|
const Sidebar = ({ className = "" }: { className?: string }) => (
|
|
<div className={`flex flex-col h-full bg-brand-navy ${className}`}>
|
|
{/* Logo */}
|
|
<div className="p-6 border-b border-white/20">
|
|
|
|
<img
|
|
src={logo}
|
|
alt="Logo"
|
|
className="h-6 md:h-8 lg:h-10 w-auto object-contain"
|
|
/>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<div className="flex-1 px-4 py-6" >
|
|
<nav className="space-y-2">
|
|
{navigationItems.map((item) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<Button
|
|
key={item.name}
|
|
variant={item.active ? "secondary" : "ghost"}
|
|
className={`w-full justify-start text-lg h-10 min-h-[44px] ${item.active
|
|
? 'bg-brand-gold text-brand-gold-foreground hover:bg-brand-gold/90'
|
|
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
|
}`}
|
|
onClick={() => {
|
|
navigate(item.href);
|
|
setSidebarOpen(false);
|
|
}}
|
|
>
|
|
<Icon className="mr-3 h-4 w-4" />
|
|
{item.name}
|
|
</Button>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
|
|
</div>
|
|
</div >
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen " style={{ backgroundColor: "green" }}>
|
|
{/* Mobile Header */}
|
|
<header className="lg:hidden fixed top-0 left-0 right-0 z-50 border-b border-border bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/60">
|
|
<div className="flex items-center justify-between p-4">
|
|
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
|
<SheetTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<Menu className="h-5 w-5" />
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="p-0 w-80">
|
|
<Sidebar />
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Corporate org badge */}
|
|
{userType === 'corporate' && user?.organization && (
|
|
<div className="flex items-center gap-2 px-3 py-1 bg-muted rounded-full">
|
|
{user.orgLogo && (
|
|
<img src={user.orgLogo} alt={user.organization} className="w-4 h-4" />
|
|
)}
|
|
<span className="text-sm font-medium">{user.organization}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Notifications */}
|
|
<div className="relative">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setNotificationsOpen(!notificationsOpen)}
|
|
className="relative"
|
|
>
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 text-xs flex items-center justify-center">
|
|
{unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
<NotificationPanel
|
|
isOpen={notificationsOpen}
|
|
onClose={() => setNotificationsOpen(false)}
|
|
notifications={notifications}
|
|
/>
|
|
</div>
|
|
|
|
{/* User menu */}
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={user?.avatar} />
|
|
<AvatarFallback className="text-sm">
|
|
{user?.name?.split(' ').map(n => n[0]).join('') || 'U'}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex">
|
|
{/* Desktop Sidebar - Reduced width from 256px to 240px */}
|
|
<div className="hidden lg:block w-60 border-r border-border">
|
|
<div className="fixed w-60 h-full">
|
|
<Sidebar />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content Area - Optimized for wider content */}
|
|
<div className="flex-1 lg:ml-0" style={{ backgroundColor: "green" }}>
|
|
{/* Desktop Header */}
|
|
<header className="hidden lg:block fixed top-0 left-60 right-0 z-40 border-b border-border bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/60">
|
|
<div className="flex items-center justify-end px-4 py-4">
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Corporate features */}
|
|
{userType === 'corporate' && (
|
|
<>
|
|
{/* Organization badge */}
|
|
{user?.organization && (
|
|
<div className="flex items-center gap-2 px-3 py-1 bg-muted rounded-full">
|
|
{user.orgLogo && (
|
|
<img src={user.orgLogo} alt={user.organization} className="w-4 h-4" />
|
|
)}
|
|
<span className="text-lg font-medium">{user.organization}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cohort reminder */}
|
|
{user?.cohort && (
|
|
<Badge variant="outline" className="text-sm">
|
|
<Users className="w-3 h-3 mr-1" />
|
|
{user.cohort}
|
|
</Badge>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Notifications */}
|
|
<div className="relative">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setNotificationsOpen(!notificationsOpen)}
|
|
className="relative"
|
|
>
|
|
<Bell className="h-5 w-5" />
|
|
{unreadCount > 0 && (
|
|
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 text-xs flex items-center justify-center">
|
|
{unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
<NotificationPanel
|
|
isOpen={notificationsOpen}
|
|
onClose={() => setNotificationsOpen(false)}
|
|
notifications={notifications}
|
|
/>
|
|
</div>
|
|
|
|
{/* User menu */}
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={user?.avatar} />
|
|
<AvatarFallback className="text-base">
|
|
{user?.name?.split(' ').map(n => n[0]).join('') || 'U'}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="hidden xl:block">
|
|
<p className="text-lg font-medium">{user?.name || 'Parth Patel'}</p>
|
|
<p className="text-sm text-muted-foreground">{user?.email || 'parthPatel@example.com'}</p>
|
|
</div>
|
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content - Scrollable area under fixed headers */}
|
|
<main
|
|
className="flex-1 h-screen bg-background overflow-hidden"
|
|
role="main"
|
|
id="main-content"
|
|
tabIndex={-1}
|
|
>
|
|
{/* Content wrapper with consistent spacing and accessibility */}
|
|
<div className="w-full h-full overflow-y-auto pt-[56px] lg:pt-[64px]">
|
|
{/* Skip to main content anchor for screen readers */}
|
|
<a
|
|
href="#learner-content"
|
|
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 bg-[#04045B] text-white px-4 py-2 rounded-lg text-base font-medium focus:outline-none focus:ring-2 focus:ring-[#F8C301] focus:ring-offset-2 transition-all duration-200"
|
|
>
|
|
Skip to learner content
|
|
</a>
|
|
|
|
{/* Main learner content area */}
|
|
<div
|
|
id="learner-content"
|
|
className="w-full h-full"
|
|
role="region"
|
|
aria-label="Learner portal content"
|
|
>
|
|
{/* Content with proper spacing and structure */}
|
|
<div className="relative">
|
|
{/* Background pattern for visual enhancement */}
|
|
<div className="absolute inset-0 opacity-[0.02] pointer-events-none">
|
|
<div className="w-full h-full bg-gradient-to-br from-[#04045B]/5 via-transparent to-[#F8C301]/5"></div>
|
|
</div>
|
|
|
|
{/* Main content with proper spacing */}
|
|
<div className="relative z-10">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Live region for dynamic content announcements */}
|
|
<div
|
|
id="learner-live-region"
|
|
aria-live="polite"
|
|
aria-atomic="true"
|
|
className="sr-only"
|
|
></div>
|
|
|
|
{/* Status region for form validation and success messages */}
|
|
<div
|
|
id="learner-status-region"
|
|
aria-live="assertive"
|
|
aria-atomic="true"
|
|
className="sr-only"
|
|
></div>
|
|
</div>
|
|
|
|
{/* Back to top functionality for long content */}
|
|
<div className="fixed bottom-6 left-6 z-40">
|
|
<button
|
|
onClick={() => {
|
|
document.getElementById('main-content')?.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}}
|
|
className="sr-only focus:not-sr-only bg-[#04045B] hover:bg-[#04045B]/90 text-white p-3 rounded-full shadow-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-[#F8C301] focus:ring-offset-2"
|
|
aria-label="Back to top of page"
|
|
title="Back to top"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Progress indicator for course content */}
|
|
<div
|
|
id="learner-progress-indicator"
|
|
className="fixed top-[56px] lg:top-[64px] left-0 right-0 h-1 bg-gray-200 opacity-0 transition-opacity duration-200 z-40"
|
|
role="progressbar"
|
|
aria-label="Page loading progress"
|
|
aria-hidden="true"
|
|
>
|
|
<div
|
|
className="h-full bg-gradient-to-r from-[#04045B] to-[#F8C301] transition-all duration-300 ease-out"
|
|
style={{ width: '0%' }}
|
|
></div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
} |