| | import React, { useState, useEffect, useCallback, memo } from 'react'; |
| | import { NavLink, useLocation } from 'react-router-dom'; |
| |
|
| | const Sidebar = ({ isCollapsed, toggleSidebar }) => { |
| | const [isMounted, setIsMounted] = useState(false); |
| | const [isLoading, setIsLoading] = useState(true); |
| | const [isHovered, setIsHovered] = useState(false); |
| | const [isAnimating, setIsAnimating] = useState(false); |
| | const [isMobile, setIsMobile] = useState(false); |
| | const [isTouchDevice, setIsTouchDevice] = useState(false); |
| | const [focusedIndex, setFocusedIndex] = useState(-1); |
| | const [isExpanded, setIsExpanded] = useState(false); |
| | const location = useLocation(); |
| |
|
| | |
| | useEffect(() => { |
| | const checkMobile = () => { |
| | const mobile = window.innerWidth < 768; |
| | const tablet = window.innerWidth >= 768 && window.innerWidth < 1024; |
| | const touch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; |
| | setIsMobile(mobile); |
| | setIsTouchDevice(touch); |
| |
|
| | |
| | if (mobile && !isCollapsed) { |
| | toggleSidebar(); |
| | } |
| | }; |
| |
|
| | checkMobile(); |
| | window.addEventListener('resize', checkMobile); |
| | return () => window.removeEventListener('resize', checkMobile); |
| | }, [isCollapsed, toggleSidebar]); |
| |
|
| | |
| | useEffect(() => { |
| | setIsMounted(true); |
| | const timer = setTimeout(() => setIsLoading(false), 300); |
| | return () => clearTimeout(timer); |
| | }, []); |
| |
|
| |
|
| | |
| | useEffect(() => { |
| | const handleGlobalKeyDown = (e) => { |
| | |
| | if ((e.ctrlKey || e.metaKey) && e.key === 'b') { |
| | e.preventDefault(); |
| | toggleSidebar(); |
| | } |
| |
|
| | |
| | if (e.key === 'Escape' && isMobile && !isCollapsed) { |
| | toggleSidebar(); |
| | } |
| | }; |
| |
|
| | document.addEventListener('keydown', handleGlobalKeyDown); |
| | return () => document.removeEventListener('keydown', handleGlobalKeyDown); |
| | }, [isCollapsed, isMobile, toggleSidebar]); |
| |
|
| | |
| | useEffect(() => { |
| | if (!isMobile && !isCollapsed) { |
| | setIsExpanded(true); |
| | } else { |
| | setIsExpanded(false); |
| | } |
| | }, [isMobile, isCollapsed]); |
| |
|
| | |
| | useEffect(() => { |
| | if (isCollapsed !== undefined) { |
| | setIsAnimating(true); |
| | const timer = setTimeout(() => setIsAnimating(false), 300); |
| | return () => clearTimeout(timer); |
| | } |
| | }, [isCollapsed]); |
| |
|
| | |
| | const menuItems = [ |
| | { |
| | path: '/dashboard', |
| | label: 'Dashboard', |
| | icon: 'dashboard', |
| | description: 'Overview and analytics', |
| | iconColor: 'text-primary-600', |
| | animationDelay: 0, |
| | ariaLabel: 'Dashboard - Overview and analytics', |
| | gradient: 'from-primary-500 to-primary-600' |
| | }, |
| | { |
| | path: '/sources', |
| | label: 'Sources', |
| | icon: 'rss_feed', |
| | description: 'Content sources management', |
| | iconColor: 'text-accent-600', |
| | animationDelay: 100, |
| | ariaLabel: 'Sources - Content sources management', |
| | gradient: 'from-accent-500 to-accent-600' |
| | }, |
| | { |
| | path: '/accounts', |
| | label: 'Accounts', |
| | icon: 'account_circle', |
| | description: 'Social media accounts', |
| | iconColor: 'text-success-600', |
| | animationDelay: 200, |
| | ariaLabel: 'Accounts - Social media accounts', |
| | gradient: 'from-success-500 to-success-600' |
| | }, |
| | { |
| | path: '/posts', |
| | label: 'Posts', |
| | icon: 'post_add', |
| | description: 'Content posts', |
| | iconColor: 'text-warning-600', |
| | animationDelay: 300, |
| | ariaLabel: 'Posts - Content posts', |
| | gradient: 'from-warning-500 to-warning-600' |
| | }, |
| | { |
| | path: '/schedule', |
| | label: 'Schedule', |
| | icon: 'schedule', |
| | description: 'Posting schedule', |
| | iconColor: 'text-info-600', |
| | animationDelay: 400, |
| | ariaLabel: 'Schedule - Posting schedule', |
| | gradient: 'from-info-500 to-info-600' |
| | } |
| | ]; |
| |
|
| | |
| | const handleKeyDown = (e, index) => { |
| | if (!e.currentTarget.classList.contains('nav-link')) return; |
| |
|
| | switch (e.key) { |
| | case 'ArrowDown': |
| | e.preventDefault(); |
| | setFocusedIndex(prev => (prev < menuItems.length - 1 ? prev + 1 : 0)); |
| | break; |
| | case 'ArrowUp': |
| | e.preventDefault(); |
| | setFocusedIndex(prev => (prev > 0 ? prev - 1 : menuItems.length - 1)); |
| | break; |
| | case 'Enter': |
| | case ' ': |
| | e.preventDefault(); |
| | e.currentTarget.click(); |
| | break; |
| | case 'Escape': |
| | if (isMobile && !isCollapsed) { |
| | toggleSidebar(); |
| | } |
| | break; |
| | default: |
| | break; |
| | } |
| | }; |
| |
|
| | |
| | const sidebarClasses = `sidebar transition-all duration-300 ease-in-out ${isCollapsed ? 'collapsed' : '' |
| | } ${isMobile ? (isCollapsed ? 'w-26' : 'w-64') : (isCollapsed ? 'w-26' : 'w-64') |
| | } ${isMounted ? 'animate-slide-in-left' : ''} ${isMobile ? 'fixed top-16 left-0 bottom-0 z-[60]' : 'fixed top-16 left-0 z-[60] h-[calc(100vh-4rem)]' |
| | } ${isExpanded ? 'shadow-xl' : ''}`; |
| |
|
| | |
| | const toggleClasses = `sidebar-toggle flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-lg transition-all duration-200 ease-in-out hover:bg-primary-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 relative overflow-hidden touch-manipulation active:scale-95 ${isMobile ? (isCollapsed ? 'mx-auto mt-2' : 'absolute top-3 right-1.5 sm:top-4 sm:right-2') : (isCollapsed ? 'mx-auto mt-2' : 'mx-auto mt-4') |
| | } backdrop-blur-sm bg-white/90 border border-transparent hover:border-primary-200 shadow-sm hover:shadow-md`; |
| |
|
| | |
| | const navClasses = `sidebar-nav h-full flex flex-col transition-all duration-300 ${isMobile ? 'justify-start pt-2 pb-4' : (isCollapsed ? 'justify-start py-1' : 'pt-8 pb-4') |
| | }`; |
| |
|
| | |
| | const navListClasses = `nav-list space-y-0 ${isMobile ? 'px-1 py-1' : (isCollapsed ? 'px-1 py-0.5' : 'px-2 py-3') |
| | }`; |
| |
|
| | |
| | const navItemClasses = (index) => `nav-item relative transition-all duration-200 ease-in-out group ${isMobile ? 'my-0.5 mx-0.5' : (isCollapsed ? 'my-0.5 mx-0.5' : 'my-1 mx-0.5') |
| | } ${focusedIndex === index ? 'ring-2 ring-primary-500 ring-offset-2' : '' |
| | } hover:bg-white/20 overflow-hidden z-10`; |
| |
|
| | |
| | const navLinkClasses = useCallback(({ isActive }) => ` |
| | nav-link group relative flex items-center px-2 sm:px-2.5 py-1.5 sm:py-2 text-xs font-medium rounded-lg transition-all duration-200 ease-in-out |
| | ${isActive |
| | ? 'bg-gradient-to-r from-primary-600 to-primary-700 text-white shadow-md transform scale-105' |
| | : 'text-secondary-700 hover:bg-accent-100 hover:shadow-sm transform hover:scale-102' |
| | } |
| | ${isMobile ? (isCollapsed ? 'justify-center px-1 sm:px-1.5 py-2 sm:py-2.5' : 'justify-start px-1 sm:px-1.5 py-1 sm:py-1.5') : (isCollapsed ? 'justify-start px-1.5' : 'justify-start px-2 py-2')} |
| | focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 |
| | disabled:opacity-50 disabled:cursor-not-allowed |
| | relative overflow-hidden |
| | before:absolute before:inset-0 before:bg-gradient-to-r before:from-primary-500 before:to-primary-600 before:opacity-0 before:transition-opacity before:duration-200 before:rounded-lg |
| | group-hover:before:opacity-10 |
| | ${isTouchDevice ? 'touch-manipulation' : ''} |
| | ${focusedIndex >= 0 ? 'focus:ring-2 focus:ring-primary-500 focus:ring-offset-2' : ''} |
| | border border-transparent hover:border-primary-200 |
| | hover:shadow-md hover:shadow-primary-500/10 |
| | active:scale-95 active:shadow-inner |
| | min-h-[36px] sm:min-h-[40px] /* Reduced touch target size */ |
| | ${isCollapsed ? 'p-0' : ''} /* Remove padding when collapsed to ensure icons are visible */ |
| | `, [isCollapsed, isMobile, isTouchDevice, focusedIndex]); |
| |
|
| | |
| | const iconClasses = ` |
| | nav-icon flex-shrink-0 w-5 h-5 transition-all duration-200 ease-in-out |
| | ${isMobile ? (isCollapsed ? 'mx-auto text-base sm:text-lg' : 'mr-2 sm:mr-3 text-base') : (isCollapsed ? 'mx-auto text-lg' : 'mr-3 text-base')} |
| | group-hover:rotate-12 group-hover:scale-110 |
| | transition-transform duration-300 ease-out |
| | shadow-sm hover:shadow-md |
| | z-20 |
| | material-icons |
| | flex items-center justify-center |
| | ${isCollapsed ? 'scale-110' : ''} /* Scale up icons when collapsed for better visibility */ |
| | `; |
| |
|
| | |
| | const labelClasses = ` |
| | nav-label transition-all duration-200 ease-in-out |
| | ${isMobile ? (isCollapsed ? 'opacity-0 max-w-0 overflow-hidden' : 'opacity-100 max-w-full text-xs sm:text-sm font-medium') : (isCollapsed ? 'opacity-100 max-w-full text-xs font-medium' : 'opacity-100 max-w-full text-sm font-medium')} |
| | group-hover:text-primary-600 |
| | transition-colors duration-200 |
| | tracking-tight |
| | text-secondary-900 |
| | font-medium |
| | ${isCollapsed && !isMobile ? 'absolute left-12 top-1/2 transform -translate-y-1/2 bg-white px-2 py-1 rounded shadow-lg z-20 whitespace-nowrap' : ''} |
| | `; |
| |
|
| | |
| | const descriptionClasses = ` |
| | nav-description text-xs text-secondary-500 mt-0.5 sm:mt-1 transition-all duration-200 ease-in-out |
| | ${isMobile ? 'hidden' : (isCollapsed ? 'opacity-0 max-w-0 overflow-hidden' : 'opacity-100 max-w-full')} |
| | group-hover:text-secondary-700 |
| | transition-colors duration-200 |
| | tracking-normal |
| | font-normal |
| | leading-relaxed |
| | `; |
| |
|
| | |
| | const badgeClasses = ` |
| | badge absolute top-0.5 right-0.5 px-1 py-0.5 text-xs font-medium rounded-full |
| | ${isMobile ? (isCollapsed ? 'opacity-0 scale-0' : 'opacity-100 scale-100') : (isCollapsed ? 'opacity-0 scale-0' : 'opacity-100 scale-100')} |
| | transition-all duration-200 ease-in-out |
| | animate-bounce-subtle |
| | bg-gradient-to-r from-primary-500 to-primary-600 text-white |
| | shadow-sm |
| | backdrop-blur-sm |
| | border border-white/20 |
| | font-semibold |
| | tracking-wide |
| | `; |
| |
|
| | |
| | const countClasses = ` |
| | count-indicator absolute top-0.5 right-0.5 w-4 h-4 sm:w-5 sm:h-5 flex items-center justify-center |
| | ${isMobile ? (isCollapsed ? 'opacity-0 scale-0' : 'opacity-100 scale-100') : (isCollapsed ? 'opacity-0 scale-0' : 'opacity-100 scale-100')} |
| | transition-all duration-200 ease-in-out |
| | animate-pulse-slow |
| | bg-gradient-to-br from-secondary-500 to-secondary-600 text-white rounded-full text-xs |
| | shadow-sm |
| | backdrop-blur-sm |
| | border border-white/20 |
| | font-semibold |
| | tracking-wide |
| | `; |
| |
|
| | |
| | const SkeletonLoader = () => ( |
| | <div className={`space-y-2.5 sm:space-y-3 p-3 sm:p-4 ${isMobile ? 'p-2' : 'p-4'}`}> |
| | {[...Array(isMobile ? 4 : 5)].map((_, index) => ( |
| | <div key={index} className="animate-pulse"> |
| | <div className="flex items-center space-x-2 sm:space-x-3"> |
| | <div className={`w-4 h-4 sm:w-5 sm:h-5 bg-gradient-to-br from-secondary-200 to-secondary-300 rounded animate-pulse ${isMobile ? 'w-3 h-3' : 'w-5 h-5'} backdrop-blur-sm`}></div> |
| | <div className="flex-1 space-y-1.5 sm:space-y-2"> |
| | <div className={`h-2.5 sm:h-3 bg-gradient-to-r from-secondary-200 to-secondary-300 rounded ${isMobile ? 'w-1/2' : 'w-3/4'} animate-pulse`}></div> |
| | {!isMobile && <div className="h-2 bg-gradient-to-r from-secondary-200 to-secondary-300 rounded w-1/2 animate-pulse"></div>} |
| | </div> |
| | </div> |
| | </div> |
| | ))} |
| | </div> |
| | ); |
| |
|
| |
|
| | |
| | const createRipple = (event) => { |
| | const button = event.currentTarget; |
| | const circle = document.createElement('span'); |
| | const diameter = Math.max(button.clientWidth, button.clientHeight); |
| | const radius = diameter / 2; |
| |
|
| | circle.style.width = circle.style.height = `${diameter}px`; |
| | circle.style.left = `${event.clientX - button.offsetLeft - radius}px`; |
| | circle.style.top = `${event.clientY - button.offsetTop - radius}px`; |
| | circle.classList.add('ripple'); |
| |
|
| | const ripple = button.getElementsByClassName('ripple')[0]; |
| | if (ripple) { |
| | ripple.remove(); |
| | } |
| |
|
| | button.appendChild(circle); |
| | }; |
| |
|
| | |
| | const handleTouchStart = (e) => { |
| | if (isTouchDevice) { |
| | e.currentTarget.classList.add('touch-active'); |
| | } |
| | }; |
| |
|
| | const handleTouchEnd = (e) => { |
| | if (isTouchDevice) { |
| | e.currentTarget.classList.remove('touch-active'); |
| | } |
| | }; |
| |
|
| | |
| | if (isMobile && !isCollapsed) { |
| | return ( |
| | <> |
| | <SkipToContent /> |
| | <div |
| | className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300" |
| | onClick={() => toggleSidebar()} |
| | aria-label="Close sidebar overlay" |
| | role="button" |
| | tabIndex={0} |
| | onKeyDown={(e) => { |
| | if (e.key === 'Enter' || e.key === ' ') { |
| | toggleSidebar(); |
| | } |
| | }} |
| | ></div> |
| | <aside |
| | className={sidebarClasses} |
| | role="navigation" |
| | aria-label="Mobile navigation" |
| | aria-modal="true" |
| | aria-expanded="true" |
| | > |
| | <button |
| | onClick={(e) => { |
| | createRipple(e); |
| | toggleSidebar(); |
| | }} |
| | className={toggleClasses} |
| | aria-label="Close sidebar" |
| | aria-expanded={false} |
| | title="Close sidebar" |
| | type="button" |
| | > |
| | <span className="text-secondary-600 group-hover:text-primary-600 transition-all duration-300"> |
| | ✕ |
| | </span> |
| | </button> |
| | |
| | <nav className={navClasses} aria-label="Main navigation"> |
| | |
| | <ul className={navListClasses} role="menu"> |
| | {menuItems.map((item, index) => ( |
| | <li |
| | key={index} |
| | className={navItemClasses(index)} |
| | role="none" |
| | style={{ animationDelay: `${item.animationDelay}ms` }} |
| | > |
| | <NavLink |
| | to={item.path} |
| | className={navLinkClasses} |
| | title={item.label} |
| | onTouchStart={handleTouchStart} |
| | onTouchEnd={handleTouchEnd} |
| | role="menuitem" |
| | aria-current={location.pathname === item.path ? 'page' : undefined} |
| | aria-label={item.ariaLabel} |
| | aria-describedby={`description-${index}`} |
| | onKeyDown={(e) => handleKeyDown(e, index)} |
| | onFocus={() => setFocusedIndex(index)} |
| | onBlur={() => setFocusedIndex(-1)} |
| | > |
| | <span className={iconContainerClasses} aria-hidden="true"> |
| | <span className={`transition-all duration-300 ease-out ${item.iconColor}`}> |
| | <i className="material-icons">{item.icon}</i> |
| | </span> |
| | </span> |
| | |
| | {!isCollapsed && ( |
| | <div className="flex-1 min-w-0 relative z-10"> |
| | <div className="flex items-center justify-between pr-2"> |
| | <span className={labelClasses}>{item.label}</span> |
| | <div className="flex items-center space-x-1"> |
| | {item.badge && ( |
| | <span className={badgeClasses} aria-label="New feature"> |
| | {item.badge} |
| | </span> |
| | )} |
| | {item.count && ( |
| | <span className={countClasses} aria-label={`${item.count} items`}> |
| | {item.count} |
| | </span> |
| | )} |
| | </div> |
| | </div> |
| | <span |
| | id={`description-${index}`} |
| | className={descriptionClasses} |
| | > |
| | {item.description} |
| | </span> |
| | </div> |
| | )} |
| | </NavLink> |
| | </li> |
| | ))} |
| | </ul> |
| | </nav> |
| | </aside> |
| | </> |
| | ); |
| | } |
| |
|
| | |
| | const SkipToContent = () => ( |
| | <a |
| | href="#main-content" |
| | className="skip-link sr-only focus:not-sr-only focus:absolute focus:top-3 sm:top-4 focus:left-3 sm:left-4 bg-primary-600 text-white px-3 sm:px-4 py-2 rounded-lg text-sm" |
| | > |
| | Skip to main content |
| | </a> |
| | ); |
| |
|
| | if (isLoading) { |
| | return ( |
| | <aside className={sidebarClasses} aria-label="Loading navigation"> |
| | <SkipToContent /> |
| | <div className="flex items-center justify-center h-full"> |
| | <SkeletonLoader /> |
| | </div> |
| | </aside> |
| | ); |
| | } |
| |
|
| | return ( |
| | <aside |
| | className={sidebarClasses} |
| | role="navigation" |
| | aria-label="Main navigation" |
| | style={{ |
| | background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.95) 100%)', |
| | backdropFilter: 'blur(10px)', |
| | borderRight: 'none', |
| | boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)' |
| | }} |
| | onMouseEnter={() => setIsHovered(true)} |
| | onMouseLeave={() => setIsHovered(false)} |
| | aria-hidden={isMobile} |
| | > |
| | <SkipToContent /> |
| | <button |
| | onClick={(e) => { |
| | createRipple(e); |
| | toggleSidebar(); |
| | }} |
| | className={toggleClasses} |
| | aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} |
| | aria-expanded={!isCollapsed} |
| | title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} |
| | type="button" |
| | > |
| | <span className="text-secondary-600 group-hover:text-primary-600 transition-all duration-300 transform group-hover:rotate-180"> |
| | {isCollapsed ? '»' : '«'} |
| | </span> |
| | <style jsx>{` |
| | .ripple { |
| | position: absolute; |
| | border-radius: 50%; |
| | background-color: rgba(145, 0, 41, 0.3); |
| | transform: scale(0); |
| | animation: ripple 600ms linear; |
| | pointer-events: none; |
| | } |
| | @keyframes ripple { |
| | to { |
| | transform: scale(4); |
| | opacity: 0; |
| | } |
| | } |
| | .touch-active { |
| | transform: scale(0.95); |
| | transition: transform 0.1s ease; |
| | } |
| | `}</style> |
| | </button> |
| | |
| | <nav className={navClasses} aria-label="Main navigation"> |
| | <ul className={navListClasses} role="menu"> |
| | {menuItems.map((item, index) => ( |
| | <li |
| | key={index} |
| | className={navItemClasses(index)} |
| | role="none" |
| | style={{ animationDelay: `${item.animationDelay}ms` }} |
| | > |
| | <NavLink |
| | to={item.path} |
| | className={navLinkClasses} |
| | title={item.label} |
| | onTouchStart={handleTouchStart} |
| | onTouchEnd={handleTouchEnd} |
| | role="menuitem" |
| | aria-current={location.pathname === item.path ? 'page' : undefined} |
| | aria-label={item.ariaLabel} |
| | aria-describedby={`description-${index}`} |
| | onKeyDown={(e) => handleKeyDown(e, index)} |
| | onFocus={() => setFocusedIndex(index)} |
| | onBlur={() => setFocusedIndex(-1)} |
| | > |
| | <span className={iconClasses} aria-hidden="true"> |
| | <span className={`transition-all duration-300 ease-out ${item.iconColor}`}> |
| | <i className="material-icons">{item.icon}</i> |
| | </span> |
| | </span> |
| | |
| | {!isCollapsed && ( |
| | <div className="flex-1 min-w-0 relative z-10"> |
| | <div className="flex items-center justify-between pr-2"> |
| | <span className={labelClasses}>{item.label}</span> |
| | <div className="flex items-center space-x-1"> |
| | {item.badge && ( |
| | <span className={badgeClasses} aria-label="New feature"> |
| | {item.badge} |
| | </span> |
| | )} |
| | {item.count && ( |
| | <span className={countClasses} aria-label={`${item.count} items`}> |
| | {item.count} |
| | </span> |
| | )} |
| | </div> |
| | </div> |
| | <span |
| | id={`description-${index}`} |
| | className={descriptionClasses} |
| | > |
| | {item.description} |
| | </span> |
| | </div> |
| | )} |
| | |
| | {!isCollapsed && ( |
| | <div className="ml-auto flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-all duration-200"> |
| | <div className={`w-1.5 h-1.5 rounded-full animate-pulse`} style={{ backgroundColor: `var(--${item.gradient.split(' ')[0].replace('from-', '')}-500)` }}></div> |
| | <div className={`w-1 h-1 rounded-full animate-ping`} style={{ backgroundColor: `var(--${item.gradient.split(' ')[0].replace('from-', '')}-400)`, animationDelay: '0.2s' }}></div> |
| | </div> |
| | )} |
| | |
| | {location.pathname === item.path && !isCollapsed && ( |
| | <div className="absolute left-0 top-0 bottom-0 w-1 rounded-r-lg animate-pulse" style={{ background: `linear-gradient(to bottom, var(--${item.gradient.split(' ')[0].replace('from-', '')}-500), var(--${item.gradient.split(' ')[1].replace('to-', '')}-600))` }}></div> |
| | )} |
| | |
| | {!isCollapsed && ( |
| | <div className="absolute inset-0 rounded-lg opacity-0 group-hover:opacity-5 transition-opacity duration-200" style={{ background: `linear-gradient(to right, var(--${item.gradient.split(' ')[0].replace('from-', '')}-500), var(--${item.gradient.split(' ')[1].replace('to-', '')}-600))` }}></div> |
| | )} |
| | </NavLink> |
| | </li> |
| | ))} |
| | </ul> |
| | </nav> |
| | |
| | {isHovered && !isCollapsed && !isMobile && ( |
| | <div className="absolute inset-0 bg-gradient-to-r from-primary-50 to-transparent opacity-30 pointer-events-none transition-opacity duration-300"></div> |
| | )} |
| | </aside> |
| | ); |
| | }; |
| |
|
| | export default memo(Sidebar); |