import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { ClipboardList, FileText, Pill, AlertTriangle, FlaskConical, FolderOpen, Send, Search, X, } from 'lucide-react' import type { ViewId } from '../types/pmr' import { useAccessibility } from '../contexts/AccessibilityContext' import { buildSearchIndex, groupResultsBySection, type SearchResult } from '../lib/search' import type { FuseResult } from 'fuse.js' interface NavItem { id: ViewId label: string icon: React.ReactNode } interface ClinicalSidebarProps { activeView: ViewId onViewChange: (view: ViewId) => void isTablet?: boolean } const navItems: NavItem[] = [ { id: 'summary', label: 'Summary', icon: }, { id: 'consultations', label: 'Experience', icon: }, { id: 'medications', label: 'Skills', icon: }, { id: 'problems', label: 'Achievements', icon: }, { id: 'investigations', label: 'Projects', icon: }, { id: 'documents', label: 'Education', icon: }, { id: 'referrals', label: 'Contact', icon: }, ] function getCurrentTime(): string { const now = new Date() return now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', }) } export function ClinicalSidebar({ activeView, onViewChange, isTablet = false }: ClinicalSidebarProps) { const [currentTime, setCurrentTime] = useState(getCurrentTime) const [searchQuery, setSearchQuery] = useState('') const [isSearchFocused, setIsSearchFocused] = useState(false) const [focusedIndex, setFocusedIndex] = useState(null) const [hoveredItem, setHoveredItem] = useState(null) const navButtonRefs = useRef<(HTMLButtonElement | null)[]>([]) const { focusAfterLoginRef, setExpandedItem } = useAccessibility() // Build search index once on mount const searchIndex = useMemo(() => buildSearchIndex(), []) const handleNavClick = useCallback( (view: ViewId) => { onViewChange(view) window.location.hash = view }, [onViewChange] ) const handleNavKeyDown = useCallback((e: React.KeyboardEvent, index: number) => { switch (e.key) { case 'ArrowDown': e.preventDefault() if (index < navItems.length - 1) { setFocusedIndex(index + 1) navButtonRefs.current[index + 1]?.focus() } break case 'ArrowUp': e.preventDefault() if (index > 0) { setFocusedIndex(index - 1) navButtonRefs.current[index - 1]?.focus() } break case 'Enter': case ' ': e.preventDefault() handleNavClick(navItems[index].id) break case 'Home': e.preventDefault() setFocusedIndex(0) navButtonRefs.current[0]?.focus() break case 'End': e.preventDefault() setFocusedIndex(navItems.length - 1) navButtonRefs.current[navItems.length - 1]?.focus() break } }, [handleNavClick]) // Update clock every minute useEffect(() => { const interval = setInterval(() => { setCurrentTime(getCurrentTime()) }, 60000) return () => clearInterval(interval) }, []) // Hash routing useEffect(() => { const handleHashChange = () => { const hash = window.location.hash.slice(1) as ViewId if (navItems.some(item => item.id === hash)) { onViewChange(hash) } } handleHashChange() window.addEventListener('hashchange', handleHashChange) return () => window.removeEventListener('hashchange', handleHashChange) }, [onViewChange]) // Alt+1-7 keyboard shortcuts and "/" for search useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.altKey && e.key >= '1' && e.key <= '7') { e.preventDefault() const index = parseInt(e.key) - 1 if (navItems[index]) { const view = navItems[index].id onViewChange(view) window.location.hash = view } } if (e.key === '/' && !isSearchFocused && document.activeElement?.tagName !== 'INPUT') { e.preventDefault() const searchInput = document.getElementById('sidebar-search') searchInput?.focus() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [onViewChange, isSearchFocused]) // Set focus-after-login ref to first nav button useEffect(() => { if (navButtonRefs.current[0]) { ;(focusAfterLoginRef as React.MutableRefObject).current = navButtonRefs.current[0] } }, [focusAfterLoginRef]) const handleSearchKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { setSearchQuery('') ;(e.target as HTMLInputElement).blur() } } const clearSearch = () => { setSearchQuery('') const searchInput = document.getElementById('sidebar-search') searchInput?.focus() } // Fuzzy search with fuse.js const searchResults = useMemo(() => { if (!searchQuery.trim() || searchQuery.length < 2) return [] const results = searchIndex.search(searchQuery) return results.slice(0, 10) // Limit to top 10 results }, [searchQuery, searchIndex]) // Group results by section for organized display const groupedResults = useMemo(() => { if (searchResults.length === 0) return new Map() return groupResultsBySection(searchResults) }, [searchResults]) const handleSearchResultClick = useCallback( (result: FuseResult) => { // Navigate to the section onViewChange(result.item.section) window.location.hash = result.item.section // Expand the matching item setExpandedItem(result.item.id) // Clear search setSearchQuery('') }, [onViewChange, setExpandedItem] ) // ── Tablet: 56px icon-only sidebar ── if (isTablet) { return ( ) } // ── Desktop: 220px full sidebar ── return ( ) }