This repository has been archived on 2026-04-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
KLC-Learners-Portal-Fronten…/src/components/learner/LearnerLayout.tsx
AnsariTufail bf6c39ea66 Fixed Ui
2025-09-09 17:38:47 +05:30

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>
);
}