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