File "StudentSidebar.tsx"
Full Path: /home/trinadezambia/public_html/student_panel/src/components/ui/pages/dashboard/StudentSidebar.tsx
File size: 18.32 KB
MIME-type: text/x-java
Charset: utf-8
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import { useTranslate } from '@/components/hooks/useTranslate';
import { useLanguage } from '@/components/hooks/useLanguage';
import {
BiHomeSmile,
BiBookOpen,
BiBookContent,
BiChat,
BiListOl,
BiNotepad,
BiClipboard,
BiDetail,
BiTask,
BiShield,
BiCalendar,
BiImages,
// BiBell,
BiSpreadsheet,
BiX,
BiGlobe,
BiChevronDown,
BiBus,
BiGroup,
BiBell,
BiInfoCircle,
BiSupport,
} from 'react-icons/bi';
import { LuNotebook } from 'react-icons/lu';
import { useGetSchoolSettings } from '@/lib/api/student/queryHooks';
interface StudentSidebarProps {
collapsed: boolean;
isMobile?: boolean;
isOpen?: boolean;
onClose?: () => void;
}
export default function StudentSidebar({
collapsed,
isMobile = false,
isOpen = false,
onClose,
}: StudentSidebarProps) {
// Get current pathname for route-based active item detection
const pathname = usePathname();
const translate = useTranslate();
// Get language state and functions from Redux
const { changeLanguage, currentLanguageName, languages } = useLanguage();
// Fetch the dynamic school logo once; reuse everywhere so desktop and mobile match backend branding
const { data: schoolSettings, isLoading: isLoadingLogo } =
useGetSchoolSettings();
const fallbackLogo = '/assets/images/common/logo.png';
const dynamicLogo = schoolSettings?.data?.settings?.horizontal_logo;
const resolvedLogo =
typeof dynamicLogo === 'string' && dynamicLogo.trim().length > 0
? dynamicLogo.trim()
: fallbackLogo;
// Language selector state for mobile
const [selectedLanguage, setSelectedLanguage] = useState(currentLanguageName);
const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false);
// Initialize selected language from Redux store
useEffect(() => {
setSelectedLanguage(currentLanguageName);
}, [currentLanguageName]);
// Get enabled features from school settings
// Features object from API: { "1": "Student Management", "2": "Academics Management", ... }
const enabledFeatures = schoolSettings?.data?.features || {};
// Helper function to check if a feature is enabled by its name
const isFeatureEnabled = (featureName: string) => {
// If features list is empty or undefined, assume no features are enabled
if (!enabledFeatures || Object.keys(enabledFeatures).length === 0) {
return false;
}
// Check if the feature name exists in the values of enabledFeatures object
return Object.values(enabledFeatures).some(
(feature) =>
typeof feature === 'string' &&
feature.toLowerCase() === featureName.toLowerCase(),
);
};
// All navigation items with their associated feature requirements
// featureRequired: name of the feature from API that must be enabled to show this item
// If featureRequired is null, the item is always shown
const allNavigationItems = [
{
id: 'home',
label: translate('home'),
icon: BiHomeSmile,
route: '/student/dashboard',
featureRequired: null, // Always show home/dashboard
},
{
id: 'subjects',
label: translate('mySubjects'),
icon: BiBookOpen,
route: '/student/subjects',
featureRequired: 'Academics Management', // Feature ID: 2
},
{
id: 'assignments',
label: translate('assignments'),
icon: BiBookContent,
route: '/student/assignments',
featureRequired: 'Assignment Management', // Feature ID: 11
},
{
id: 'chats',
label: translate('chats'),
icon: BiChat,
route: '/student/chats',
featureRequired: 'Chat Module', // Feature ID: 20
},
{
id: 'timetable',
label: translate('timetable'),
icon: BiListOl,
route: '/student/timetable',
featureRequired: 'Timetable Management', // Feature ID: 7
},
{
id: 'noticeboard',
label: translate('noticeboard'),
icon: BiNotepad,
route: '/student/noticeboard',
featureRequired: 'Announcement Management', // Feature ID: 12
},
{
id: 'exams',
label: translate('exams'),
icon: BiClipboard,
route: '/student/exams',
featureRequired: 'Exam Management', // Feature ID: 9
},
{
id: 'result',
label: translate('result'),
icon: BiDetail,
route: '/student/result',
featureRequired: 'Exam Management', // Feature ID: 9 (results are part of exam management)
},
{
id: 'report',
label: translate('report'),
icon: BiTask,
route: '/student/report',
featureRequired: ['Exam Management', 'Assignment Management'], // Show if either is enabled
},
{
id: 'diary',
label: translate('myDiary'),
icon: LuNotebook,
route: '/student/diary',
featureRequired: 'Student Management', // Feature ID: 1 (diary is related to student management)
},
{
id: 'transportation',
label: translate('transportation'),
icon: BiBus,
route: '/student/transportation',
featureRequired: 'Transportation Module', // Feature ID: 21
},
{
id: 'teachers',
label: translate('teachers'),
icon: BiGroup,
route: '/student/teachers',
featureRequired: 'Teacher Management', // Feature ID: 4
},
{
id: 'holiday',
label: translate('holiday'),
icon: BiCalendar,
route: '/student/holiday',
featureRequired: 'Holiday Management', // Feature ID: 6
},
{
id: 'gallery',
label: translate('gallery'),
icon: BiImages,
route: '/student/gallery',
featureRequired: 'School Gallery Management', // Feature ID: 17
},
{
id: 'guardian',
label: translate('guardianDetails'),
icon: BiBookContent,
route: '/student/guardian',
featureRequired: null, // Always show guardian details
},
{
id: 'notifications',
label: translate('notifications'),
icon: BiBell,
route: '/student/notifications',
featureRequired: 'Announcement Management', // Feature ID: 12
},
{
id: 'privacy',
label: translate('privacyPolicy'),
icon: BiShield,
route: '/student/privacy',
featureRequired: null, // Always show privacy policy
},
{
id: 'terms',
label: translate('termsAndCondition'),
icon: BiSpreadsheet,
route: '/student/terms',
featureRequired: null, // Always show terms and conditions
},
{
id: 'about-us',
label: translate('aboutUs'),
icon: BiInfoCircle,
route: '/student/about-us',
featureRequired: null, // Always show about us
},
{
id: 'contact-us',
label: translate('contactUs'),
icon: BiSupport,
route: '/student/contact-us',
featureRequired: null, // Always show contact us
},
];
// Filter navigation items based on enabled features
// Only show items where the required feature is enabled or no feature is required
const navigationItems = allNavigationItems.filter((item) => {
// If no feature is required, always show the item
if (!item.featureRequired) {
return true;
}
// Check if the required feature is enabled
// Support validation for multiple features (OR condition)
if (Array.isArray(item.featureRequired)) {
return item.featureRequired.some((feature) => isFeatureEnabled(feature));
}
// Single feature check
return isFeatureEnabled(item.featureRequired);
});
// Function to determine active item based on current route
const getActiveItem = () => {
// Check for exact route matches first
const exactMatch = navigationItems.find((item) => item.route === pathname);
if (exactMatch) {
return exactMatch.id;
}
// Check for partial matches (e.g., /student/dashboard/profile should match dashboard)
const partialMatch = navigationItems.find(
(item) => pathname.startsWith(item.route) && item.route !== '/student',
);
if (partialMatch) {
return partialMatch.id;
}
// Default to home if no match found
return 'home';
};
// Get the current active item based on route
const activeItem = getActiveItem();
// Handle mobile drawer close on item click
const handleItemClick = (itemId: string) => {
// Find the route for the clicked item and navigate
const item = navigationItems.find((navItem) => navItem.id === itemId);
if (item) {
// Navigation will be handled by Next.js Link components
// Just close the mobile drawer if needed
if (isMobile && onClose) {
onClose();
}
}
};
// Handle language selection - update Redux store and close dropdown
const handleLanguageSelect = (languageName: string) => {
// Find the language code from the language name
const selectedLang = languages.find((lang) => lang.name === languageName);
if (selectedLang) {
// Update language in Redux store
changeLanguage(selectedLang.code);
setSelectedLanguage(selectedLang.name);
}
setIsLanguageDropdownOpen(false);
// Close mobile sidebar after language selection (like other navigation items)
if (isMobile && onClose) {
onClose();
}
};
// Handle escape key to close mobile drawer
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isMobile && isOpen && onClose) {
onClose();
}
};
if (isMobile && isOpen) {
document.addEventListener('keydown', handleEscape);
// Prevent body scroll when drawer is open
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isMobile, isOpen, onClose]);
// Mobile drawer overlay
if (isMobile) {
return (
<>
{/* Overlay */}
{isOpen && (
<div
className="fixed inset-0 bg-(--light-primary-color) bg-opacity-50 z-40 lg:hidden"
onClick={onClose}
/>
)}
{/* Mobile Drawer - Responsive width for different mobile sizes */}
{/* Using explicit LTR/RTL variants for proper positioning */}
<div
className={`mobile-drawer fixed top-0 ltr:left-0 rtl:right-0 h-screen w-72 sm:w-80 bg-white z-50 transform transition-transform duration-300 ease-in-out lg:hidden ${
isOpen
? 'translate-x-0'
: 'ltr:-translate-x-full rtl:translate-x-full'
}`}
onClick={(e) => e.stopPropagation()} // Prevent clicks inside drawer from bubbling to overlay
>
<div className="flex flex-col h-full p-4 sm:p-6 ltr:border-r rtl:border-l border-gray-200">
{/* Mobile Header with Close Button - Responsive sizing */}
<div className="shrink-0 flex items-center justify-between mb-6 sm:mb-8">
<div className="flex items-center gap-2 w-[120px] h-[42px] sm:w-[140px] sm:h-[50px]">
<Image
src={resolvedLogo}
alt="Logo"
width={0}
height={0}
className="w-full h-full object-contain"
priority
/>
</div>
<button
onClick={onClose}
className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<BiX className="w-5 h-5 sm:w-6 sm:h-6 text-gray-600" />
</button>
</div>
{/* Navigation Menu - Responsive sizing */}
<nav className="flex-1 min-h-0 overflow-y-auto space-y-1 scrollbar-thin scrollbar-track-gray-100 scrollbar-thumb-gray-400 hover:scrollbar-thumb-gray-500 mb-20">
{navigationItems.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.id}
href={item.route}
onClick={() => handleItemClick(item.id)}
className={`w-full flex items-center text-base font-normal cursor-pointer gap-2 sm:gap-3 px-3 sm:px-4 py-2.5 sm:py-3 rounded-lg transition-colors text-left ${
activeItem === item.id
? 'bg-(--primary-color) text-white'
: 'text-black hover:bg-(--primary-color) hover:text-white'
}`}
>
<Icon className="w-4 h-4 sm:w-5 sm:h-5 shrink-0" />
<span className="text-xs sm:text-sm font-medium">
{item.label}
</span>
</Link>
);
})}
{/* Language Selector - Mobile Only - Responsive sizing */}
<div className="relative">
{/* Language Dropdown Container */}
<div className="relative">
<button
onClick={(e) => {
e.stopPropagation(); // Prevent event bubbling to avoid closing sidebar
setIsLanguageDropdownOpen(!isLanguageDropdownOpen);
}}
className="w-full flex items-center justify-between px-3 sm:px-4 py-2.5 sm:py-3 text-left"
>
<div className="flex items-center gap-2 sm:gap-3">
<BiGlobe className="w-4 h-4 sm:w-5 sm:h-5 text-gray-600" />
<span className="text-xs sm:text-sm font-medium text-gray-900">
{selectedLanguage}
</span>
</div>
<BiChevronDown
className={`w-3.5 h-3.5 sm:w-4 sm:h-4 text-gray-500 transition-transform ${
isLanguageDropdownOpen ? 'rotate-180' : ''
}`}
/>
</button>
{/* Language Dropdown Options - Responsive sizing */}
{isLanguageDropdownOpen && (
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-gray-300 rounded-lg shadow-lg z-10 max-h-48 overflow-y-auto">
{languages.map((language) => (
<button
key={language.code}
onClick={(e) => {
e.stopPropagation(); // Prevent event bubbling to avoid closing sidebar
handleLanguageSelect(language.name);
}}
className={`w-full flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-2.5 sm:py-3 text-left hover:bg-gray-50 transition-colors first:rounded-t-lg last:rounded-b-lg ${
selectedLanguage === language.name
? 'bg-gray-100'
: ''
}`}
>
<span className="text-base sm:text-lg">
{language.flag}
</span>
<span className="text-xs sm:text-sm font-medium text-gray-900">
{language.name}
</span>
</button>
))}
</div>
)}
</div>
</div>
</nav>
</div>
</div>
</>
);
}
// Desktop sidebar - Responsive width for tablets and desktop
// Using explicit LTR/RTL variants for proper positioning
return (
<div
className={`${
collapsed ? 'w-14 md:w-16' : 'w-64 md:w-76'
} bg-white text-black h-screen fixed ltr:left-0 rtl:right-0 top-0 z-50 transition-all duration-300 ltr:border-r rtl:border-l border-[#EAEAEA] hidden md:block`}
>
<div className="">
{/* Logo Section - Responsive sizing for tablet and desktop */}
{/* Using gap instead of space-x for RTL support */}
<div
className={`flex items-center ${
collapsed ? 'justify-center' : 'gap-2 px-4 md:px-6 pt-4 md:pt-6'
} mb-[10px]`}
>
{!collapsed && (
<span className="h-[50px] w-[150px] md:h-[60px] md:w-[180px] flex items-center justify-center overflow-hidden">
{isLoadingLogo ? (
// Loading skeleton for logo - matches logo dimensions with pulse animation
<div className="w-full h-full bg-gray-200 rounded animate-pulse" />
) : (
<Link href="/student/dashboard">
<Image
src={resolvedLogo}
alt="Logo"
width={0}
height={0}
className="w-full h-full object-contain aspect-auto"
priority
/>
</Link>
)}
</span>
)}
</div>
{!collapsed && <div className="h-px w-full bg-gray-200 mb-3"></div>}
{/* Navigation Menu - Responsive sizing for tablet and desktop */}
<nav
className={`space-y-2 overflow-y-auto scrollbar-thin scrollbar-track-gray-100 scrollbar-thumb-gray-400 ${
collapsed
? 'pb-0 h-[calc(100vh-100px)] px-1.5 md:px-2'
: 'pb-4 md:pb-6 h-[calc(100vh-150px)] px-4 md:px-6'
}`}
>
{navigationItems.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.id}
href={item.route}
className={`w-full flex items-center text-base font-normal cursor-pointer ${
collapsed
? 'justify-center px-1.5 md:px-2'
: 'gap-2 md:gap-3 px-3 md:px-4'
} py-2.5 md:py-3 rounded-lg transition-colors text-left ${
activeItem === item.id
? 'bg-(--primary-color) text-white'
: 'text-black hover:bg-(--primary-color) hover:text-white'
}`}
title={collapsed ? item.label : undefined}
>
<Icon className="w-4 h-4 md:w-5 md:h-5 shrink-0" />
{!collapsed && (
<span className="text-xs md:text-sm font-medium">
{item.label}
</span>
)}
</Link>
);
})}
</nav>
</div>
</div>
);
}