474 lines
20 KiB
TypeScript
474 lines
20 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState, useEffect } from 'react'
|
||
import { useRouter, usePathname } from 'next/navigation'
|
||
import Image from 'next/image'
|
||
import useUIStore from '../../app/store/uiStore'
|
||
import useNavigationStore from '../../app/store/navigationStore'
|
||
import { useNavigationService } from '@/services/navigationService'
|
||
|
||
interface NavigationItem {
|
||
id: number
|
||
label: string
|
||
icon: React.ComponentType<{ className?: string }>
|
||
}
|
||
|
||
interface SidebarProps {
|
||
navigationItems?: NavigationItem[]
|
||
logoSrc?: string
|
||
userInfo?: {
|
||
name: string
|
||
role: string
|
||
avatar?: string
|
||
}
|
||
activeItem?: number | null
|
||
onCustomItemClick?: (itemId: number) => boolean
|
||
}
|
||
|
||
// Иконки-заглушки
|
||
const BookOpen = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M12 2L3 7v10c0 5.55 3.84 10 9 10s9-4.45 9-10V7l-9-5z" />
|
||
</svg>
|
||
)
|
||
|
||
const Bot = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
|
||
</svg>
|
||
)
|
||
|
||
const SquareTerminal = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z" />
|
||
</svg>
|
||
)
|
||
|
||
const CircleDot = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<circle cx="12" cy="12" r="10" />
|
||
<circle cx="12" cy="12" r="3" />
|
||
</svg>
|
||
)
|
||
|
||
const BellDot = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M12 2C8.13 2 5 5.13 5 9v7l-2 2v1h16v-1l-2-2V9c0-3.87-3.13-7-7-7z" />
|
||
</svg>
|
||
)
|
||
|
||
const History = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z" />
|
||
</svg>
|
||
)
|
||
|
||
const Settings2 = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" />
|
||
</svg>
|
||
)
|
||
|
||
const Monitor = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M20 3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h3l-1 1v1h12v-1l-1-1h3c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 13H4V5h16v11z" />
|
||
<circle cx="8" cy="10" r="2" />
|
||
<circle cx="16" cy="10" r="2" />
|
||
</svg>
|
||
)
|
||
|
||
const Building = ({ className }: { className?: string }) => (
|
||
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M12 2L2 7v10c0 5.55 3.84 10 9 10s9-4.45 9-10V7l-9-5zM12 4.14L18 7.69V17c0 3.31-2.69 6-6 6s-6-2.69-6-6V7.69L12 4.14z" />
|
||
<rect x="8" y="10" width="2" height="2" />
|
||
<rect x="14" y="10" width="2" height="2" />
|
||
<rect x="8" y="14" width="2" height="2" />
|
||
<rect x="14" y="14" width="2" height="2" />
|
||
</svg>
|
||
)
|
||
|
||
// основные routes
|
||
const mainNavigationItems: NavigationItem[] = [
|
||
{
|
||
id: 1,
|
||
icon: BookOpen,
|
||
label: 'Дашборд'
|
||
},
|
||
{
|
||
id: 2,
|
||
icon: Bot,
|
||
label: 'Навигация по зданию'
|
||
},
|
||
{
|
||
id: 8,
|
||
icon: History,
|
||
label: 'История тревог'
|
||
},
|
||
{
|
||
id: 9,
|
||
icon: Settings2,
|
||
label: 'Отчеты'
|
||
}
|
||
]
|
||
|
||
// суб-меню под "Навигация по зданию"
|
||
const navigationSubItems: NavigationItem[] = [
|
||
{
|
||
id: 3,
|
||
icon: Monitor,
|
||
label: 'Зоны Мониторинга'
|
||
},
|
||
{
|
||
id: 4,
|
||
icon: Building,
|
||
label: 'Навигация по этажам'
|
||
},
|
||
{
|
||
id: 5,
|
||
icon: BellDot,
|
||
label: 'Уведомления'
|
||
},
|
||
{
|
||
id: 6,
|
||
icon: CircleDot,
|
||
label: 'Сенсоры'
|
||
},
|
||
{
|
||
id: 7,
|
||
icon: SquareTerminal,
|
||
label: 'Список датчиков'
|
||
}
|
||
]
|
||
|
||
|
||
const Sidebar: React.FC<SidebarProps> = ({
|
||
logoSrc,
|
||
userInfo = {
|
||
name: 'Александр',
|
||
role: 'Администратор'
|
||
},
|
||
activeItem: propActiveItem,
|
||
onCustomItemClick
|
||
}) => {
|
||
const navigationService = useNavigationService()
|
||
const router = useRouter()
|
||
const pathname = usePathname()
|
||
const [internalActiveItem, setInternalActiveItem] = useState<number | null>(null)
|
||
const [isHydrated, setIsHydrated] = useState(false)
|
||
const [manuallyToggled, setManuallyToggled] = useState(false)
|
||
const activeItem = propActiveItem !== undefined ? propActiveItem : internalActiveItem
|
||
const {
|
||
isSidebarCollapsed: isCollapsed,
|
||
toggleSidebar,
|
||
isNavigationSubMenuExpanded: showNavigationSubItems,
|
||
setNavigationSubMenuExpanded: setShowNavigationSubItems,
|
||
toggleNavigationSubMenu
|
||
} = useUIStore()
|
||
|
||
const {
|
||
openMonitoring,
|
||
openFloorNavigation,
|
||
openNotifications,
|
||
closeMonitoring,
|
||
closeFloorNavigation,
|
||
closeNotifications,
|
||
showMonitoring,
|
||
showFloorNavigation,
|
||
showNotifications
|
||
} = useNavigationStore()
|
||
|
||
useEffect(() => {
|
||
setIsHydrated(true)
|
||
}, [])
|
||
|
||
// Чек если суб-меню активны
|
||
const isNavigationSubItemActive = activeItem && [3, 4, 5, 6, 7].includes(activeItem)
|
||
const shouldShowNavigationAsActive = activeItem === 2 || isNavigationSubItemActive
|
||
|
||
// Авто-расткрытие меню, если суб-меню стало активным (только если не было ручного переключения)
|
||
useEffect(() => {
|
||
if (isNavigationSubItemActive && !showNavigationSubItems && !manuallyToggled) {
|
||
setShowNavigationSubItems(true)
|
||
}
|
||
}, [isNavigationSubItemActive, showNavigationSubItems, manuallyToggled, setShowNavigationSubItems])
|
||
|
||
const handleItemClick = (itemId: number) => {
|
||
let handled = false
|
||
|
||
// Handle submenu items directly with navigation store
|
||
switch (itemId) {
|
||
case 2: // Navigation - only navigate to page, don't open submenus
|
||
if (pathname !== '/navigation') {
|
||
router.push('/navigation')
|
||
}
|
||
handled = true
|
||
break
|
||
case 3: // Monitoring
|
||
if (pathname !== '/navigation') {
|
||
// Navigate to navigation page first, then open monitoring
|
||
router.push('/navigation')
|
||
setTimeout(() => openMonitoring(), 100)
|
||
} else if (showMonitoring) {
|
||
// Close if already open
|
||
closeMonitoring()
|
||
} else {
|
||
openMonitoring()
|
||
}
|
||
handled = true
|
||
break
|
||
case 4: // Floor Navigation
|
||
if (pathname !== '/navigation') {
|
||
// Navigate to navigation page first, then open floor navigation
|
||
router.push('/navigation')
|
||
setTimeout(() => openFloorNavigation(), 100)
|
||
} else if (showFloorNavigation) {
|
||
// Close if already open
|
||
closeFloorNavigation()
|
||
} else {
|
||
openFloorNavigation()
|
||
}
|
||
handled = true
|
||
break
|
||
case 5: // Notifications
|
||
if (pathname !== '/navigation') {
|
||
// Navigate to navigation page first, then open notifications
|
||
router.push('/navigation')
|
||
setTimeout(() => openNotifications(), 100)
|
||
} else if (showNotifications) {
|
||
// Close if already open
|
||
closeNotifications()
|
||
} else {
|
||
openNotifications()
|
||
}
|
||
handled = true
|
||
break
|
||
default:
|
||
// For other items, use navigation service
|
||
if (navigationService) {
|
||
handled = navigationService.handleSidebarItemClick(itemId, pathname)
|
||
}
|
||
break
|
||
}
|
||
|
||
if (handled) {
|
||
// Update internal active item state
|
||
if (propActiveItem === undefined) {
|
||
setInternalActiveItem(itemId)
|
||
}
|
||
|
||
// Call custom handler if provided (for additional logic)
|
||
if (onCustomItemClick) {
|
||
onCustomItemClick(itemId)
|
||
}
|
||
}
|
||
}
|
||
|
||
return (
|
||
<aside
|
||
className={`flex flex-col items-start gap-6 relative bg-[#161824] transition-all duration-300 h-screen ${
|
||
isCollapsed ? 'w-16' : 'w-64'
|
||
}`}
|
||
role="navigation"
|
||
aria-label="Main navigation"
|
||
>
|
||
<header className="flex items-center gap-2 pt-2 pb-2 px-4 relative self-stretch w-full flex-[0_0_auto] bg-[#161824]">
|
||
{!isCollapsed && (
|
||
<div className="relative w-[169.83px] h-[34px]">
|
||
{logoSrc && (
|
||
<Image
|
||
className="absolute w-12 h-[33px] top-0 left-0"
|
||
alt="AerBIM Monitor Logo"
|
||
src={logoSrc}
|
||
width={48}
|
||
height={33}
|
||
/>
|
||
)}
|
||
<div className="absolute w-[99px] top-[21px] left-[50px] [font-family:'Open_Sans-Regular',Helvetica] font-normal text-[#389ee8] text-[13px] tracking-[0] leading-[13px] whitespace-nowrap">
|
||
AMS HT Viewer
|
||
</div>
|
||
<div className="absolute top-px left-[50px] [font-family:'Open_Sans-SemiBold',Helvetica] font-semibold text-[#f1f6fa] text-[15px] tracking-[0] leading-[15px] whitespace-nowrap">
|
||
AerBIM Monitor
|
||
</div>
|
||
</div>
|
||
)}
|
||
</header>
|
||
|
||
<nav className="flex flex-col items-end gap-2 relative flex-1 self-stretch w-full grow">
|
||
<div className="flex flex-col items-end gap-2 px-4 py-2 relative self-stretch w-full flex-[0_0_auto]">
|
||
<ul className="flex flex-col items-start gap-3 relative self-stretch w-full flex-[0_0_auto]" role="list">
|
||
{mainNavigationItems.map((item) => {
|
||
const IconComponent = item.icon
|
||
const isActive = item.id === 2 ? shouldShowNavigationAsActive : activeItem === item.id
|
||
|
||
return (
|
||
<li key={item.id} className="flex-col flex items-center relative self-stretch w-full" role="listitem">
|
||
<button
|
||
className={`gap-2 pt-2 pr-2 pb-2 pl-2 rounded-md flex h-9 items-center relative self-stretch w-full transition-colors duration-200 hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset ${
|
||
isActive ? 'bg-gray-700' : ''
|
||
}`}
|
||
onClick={() => handleItemClick(item.id)}
|
||
aria-current={isActive ? 'page' : undefined}
|
||
type="button"
|
||
>
|
||
<IconComponent
|
||
className="!relative !w-5 !h-5 text-white"
|
||
aria-hidden="true"
|
||
/>
|
||
{!isCollapsed && (
|
||
<span className="flex-1 [font-family:'Inter-Regular',Helvetica] font-normal text-white text-sm leading-[14px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical] text-left">
|
||
{item.label}
|
||
</span>
|
||
)}
|
||
{item.id === 2 && !isCollapsed && (
|
||
<div
|
||
className="p-1 hover:bg-gray-600 rounded transition-colors duration-200 cursor-pointer"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
setManuallyToggled(true)
|
||
// Close all active submenus when collapsing navigation menu
|
||
if (showNavigationSubItems || isNavigationSubItemActive) {
|
||
closeMonitoring()
|
||
closeFloorNavigation()
|
||
closeNotifications()
|
||
}
|
||
toggleNavigationSubMenu()
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
setManuallyToggled(true)
|
||
// Close all active submenus when collapsing navigation menu
|
||
if (showNavigationSubItems || isNavigationSubItemActive) {
|
||
closeMonitoring()
|
||
closeFloorNavigation()
|
||
closeNotifications()
|
||
}
|
||
toggleNavigationSubMenu()
|
||
}
|
||
}}
|
||
aria-label={isHydrated ? (showNavigationSubItems || isNavigationSubItemActive ? 'Collapse navigation menu' : 'Expand navigation menu') : 'Toggle navigation menu'}
|
||
>
|
||
<svg
|
||
className={`!relative !w-4 !h-4 text-white transition-transform duration-200 ${
|
||
isHydrated && (showNavigationSubItems || isNavigationSubItemActive) ? 'rotate-90' : ''
|
||
}`}
|
||
fill="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
|
||
</svg>
|
||
</div>
|
||
)}
|
||
</button>
|
||
|
||
{/* Суб-меню */}
|
||
{item.id === 2 && !isCollapsed && (showNavigationSubItems || isNavigationSubItemActive) && (
|
||
<ul className="flex flex-col items-start gap-1 mt-1 ml-6 relative w-full" role="list">
|
||
{navigationSubItems.map((subItem) => {
|
||
const SubIconComponent = subItem.icon
|
||
const isSubActive = activeItem === subItem.id
|
||
|
||
return (
|
||
<li key={subItem.id} className="flex-col flex h-8 items-center relative self-stretch w-full" role="listitem">
|
||
<button
|
||
className={`gap-2 pt-1.5 pr-2 pb-1.5 pl-2 rounded-md flex h-8 items-center relative self-stretch w-full transition-colors duration-200 hover:bg-gray-600 focus:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset ${
|
||
isSubActive ? 'bg-gray-600' : ''
|
||
}`}
|
||
onClick={() => handleItemClick(subItem.id)}
|
||
aria-current={isSubActive ? 'page' : undefined}
|
||
type="button"
|
||
>
|
||
<SubIconComponent
|
||
className="!relative !w-4 !h-4 text-gray-300"
|
||
aria-hidden="true"
|
||
/>
|
||
<span className="flex-1 [font-family:'Inter-Regular',Helvetica] font-normal text-gray-300 text-sm leading-[14px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical] text-left">
|
||
{subItem.label}
|
||
</span>
|
||
</button>
|
||
</li>
|
||
)
|
||
})}
|
||
</ul>
|
||
)}
|
||
</li>
|
||
)
|
||
})}
|
||
</ul>
|
||
<button
|
||
className="!relative !w-6 !h-6 p-1 rounded hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200"
|
||
onClick={() => {
|
||
// If navigation submenu is open, first collapse it (which closes all submenus)
|
||
if (showNavigationSubItems) {
|
||
setShowNavigationSubItems(false)
|
||
setManuallyToggled(true)
|
||
// Close all active submenus when collapsing navigation menu
|
||
closeMonitoring()
|
||
closeFloorNavigation()
|
||
closeNotifications()
|
||
}
|
||
// Always collapse the sidebar
|
||
toggleSidebar()
|
||
}}
|
||
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||
type="button"
|
||
>
|
||
<svg className={`!relative !w-4 !h-4 text-white transition-transform duration-200 ${isCollapsed ? 'rotate-180' : ''}`} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</nav>
|
||
|
||
{!isCollapsed && (
|
||
<footer className="flex w-64 items-center gap-2 pt-2 pr-2 pb-2 pl-2 mt-auto bg-[#161824]">
|
||
<div className="inline-flex flex-[0_0_auto] items-center gap-2 relative rounded-md">
|
||
<div className="inline-flex items-center relative flex-[0_0_auto]">
|
||
<div
|
||
className="relative w-8 h-8 rounded-lg bg-white"
|
||
role="img"
|
||
aria-label="User avatar"
|
||
>
|
||
{userInfo.avatar && (
|
||
<Image
|
||
src={userInfo.avatar}
|
||
alt="User avatar"
|
||
className="w-full h-full rounded-lg object-cover"
|
||
fill
|
||
sizes="32px"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex p-2 flex-1 grow items-center gap-2 relative rounded-md">
|
||
<div className="flex flex-col items-start justify-center gap-0.5 relative flex-1 grow">
|
||
<div className="self-stretch mt-[-1.00px] [font-family:'Inter-SemiBold',Helvetica] font-semibold text-white text-sm leading-[14px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical]">
|
||
{userInfo.name}
|
||
</div>
|
||
<div className="self-stretch [font-family:'Inter-Regular',Helvetica] font-normal text-[#71717a] text-[10px] leading-[10px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical]">
|
||
{userInfo.role}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
className="relative w-4 h-4 aspect-[1] p-1 rounded hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-200"
|
||
aria-label="User menu"
|
||
type="button"
|
||
>
|
||
<svg className="w-3.5 h-3.5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
||
</svg>
|
||
</button>
|
||
</footer>
|
||
)}
|
||
</aside>
|
||
)
|
||
}
|
||
|
||
export default Sidebar |