import { Box, VStack, Text, useColorModeValue, Icon, Link as ChakraLink, Divider, Image, Flex, Spinner } from '@chakra-ui/react'; import { Link as RouterLink, useLocation } from 'react-router-dom'; import { useEffect, useRef, useCallback, useState } from 'react'; import { FaTachometerAlt, FaUsers, FaFutbol, FaCalendarAlt, FaNewspaper, FaHandshake, FaImage, FaEnvelope, FaCog, FaPalette, FaHome, FaSignOutAlt, FaPaperPlane, FaAward, FaSyncAlt, FaBook, FaMobileAlt, FaChartBar, FaFolder, FaAddressBook, FaBars, FaPoll, FaPaintBrush, FaVideo, FaCamera, FaTshirt, FaBullhorn, FaUserShield, FaFileAlt } from 'react-icons/fa'; import { useAuth } from '../../contexts/AuthContext'; import { useQuery } from '@tanstack/react-query'; import { getUpcomingEvents } from '../../services/eventService'; import { getAllNavigationItems, NavigationItem, seedDefaultNavigation } from '../../services/navigation'; interface NavItemProps { icon: any; to?: string; children: React.ReactNode; onClick?: (e?: React.MouseEvent) => void; } const NavItem = ({ icon, to, children, onClick }: NavItemProps) => { const location = useLocation(); const isActive = to ? location.pathname.startsWith(to) : false; const activeBg = useColorModeValue('blue.50', 'blue.900'); const activeColor = useColorModeValue('blue.600', 'blue.300'); const handleClick = (e: React.MouseEvent) => { // Call the onClick handler first if (onClick) { onClick(e); // If onClick called preventDefault, respect it if (e.isDefaultPrevented()) { return; } } // Allow RouterLink to handle navigation normally }; // If onClick is provided without `to`, render as a button-like link const LinkComponent = to ? RouterLink : 'a'; const linkProps = to ? { to } : { href: '#' }; return ( {children} ); }; interface AdminSidebarProps { isOpen: boolean; onClose: () => void; bg?: string; borderRight?: string; borderColor?: string; } // Icon mapping for navigation items const getIconForPageType = (pageType?: string): any => { const iconMap: Record = { dashboard: FaTachometerAlt, analytics: FaChartBar, teams: FaUsers, matches: FaCalendarAlt, activities: FaCalendarAlt, players: FaFutbol, articles: FaNewspaper, categories: FaFileAlt, about: FaBook, videos: FaVideo, gallery: FaImage, scoreboard: FaTachometerAlt, scoreboard_remote: FaMobileAlt, clothing: FaTshirt, sponsors: FaHandshake, banners: FaBullhorn, messages: FaEnvelope, contacts: FaAddressBook, newsletter: FaPaperPlane, polls: FaPoll, navigation: FaBars, competition_aliases: FaAward, prefetch: FaSyncAlt, users: FaUserShield, settings: FaPalette, files: FaFolder, docs: FaBook, }; return iconMap[pageType || ''] || FaFileAlt; }; const AdminSidebar = ({ isOpen, onClose, bg: bgProp, borderRight = '1px', borderColor: borderColorProp }: AdminSidebarProps) => { const { logout, user } = useAuth(); const isAdmin = (user as any)?.role === 'admin'; const defaultBg = useColorModeValue('white', '#1a1d29'); const defaultBorderColor = useColorModeValue('gray.200', 'rgba(255, 255, 255, 0.12)'); const textColor = useColorModeValue('gray.800', '#e2e8f0'); const bg = bgProp || defaultBg; const borderColor = borderColorProp || defaultBorderColor; // Upcoming events count for badge const { data: upcomingEvents } = useQuery({ queryKey: ['admin-sidebar-upcoming-events'], queryFn: getUpcomingEvents }); const upcomingCount = Array.isArray(upcomingEvents) ? upcomingEvents.length : 0; const scrollRef = useRef(null); const location = useLocation(); const STORAGE_KEY = 'admin-sidebar-scroll'; // Dynamic navigation state const [navItems, setNavItems] = useState([]); const [navLoading, setNavLoading] = useState(true); // Restore scroll on mount useEffect(() => { const node = scrollRef.current; if (!node) return; const saved = sessionStorage.getItem(STORAGE_KEY); if (saved) { try { const top = parseInt(saved, 10); if (!Number.isNaN(top)) { node.scrollTop = top; } } catch {} } }, []); // Save scroll on scroll const handleScroll = useCallback(() => { const node = scrollRef.current; if (!node) return; sessionStorage.setItem(STORAGE_KEY, String(node.scrollTop)); }, []); // Load dynamic navigation from API useEffect(() => { let active = true; (async () => { try { const items = await getAllNavigationItems(); if (active && Array.isArray(items)) { // Filter only admin navigation items const adminItems = items.filter(item => item.requires_admin); // Auto-seed if admin navigation is empty and user is admin if (adminItems.length === 0 && isAdmin) { try { console.log('Admin navigation empty, auto-seeding...'); await seedDefaultNavigation(); const newItems = await getAllNavigationItems(); if (active && Array.isArray(newItems)) { const newAdminItems = newItems.filter(item => item.requires_admin); setNavItems(newAdminItems); } } catch (seedError) { console.error('Auto-seed failed:', seedError); // Continue with empty navigation (will show fallback) setNavItems(adminItems); } } else { setNavItems(adminItems); } } } catch (error) { console.error('Failed to load admin navigation:', error); } finally { if (active) setNavLoading(false); } })(); return () => { active = false }; }, [isAdmin]); // Keep active item in view upon route change - but only if it's not visible useEffect(() => { const node = scrollRef.current; if (!node) return; const active = node.querySelector('[data-navitem][data-active="true"]') as HTMLElement | null; if (active) { // Check if the active item is already visible in the viewport const containerRect = node.getBoundingClientRect(); const activeRect = active.getBoundingClientRect(); const isVisible = ( activeRect.top >= containerRect.top && activeRect.bottom <= containerRect.bottom ); // Only scroll if the active item is not fully visible if (!isVisible) { active.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' }); } } }, [location.pathname]); return ( Club Logo My Club Admin Panel Zpět na web {/* Dynamic Navigation */} {navLoading ? ( ) : navItems.length > 0 ? ( // Render dynamic navigation <> {navItems.filter(item => item.visible).map((item, index) => { const itemIcon = getIconForPageType(item.page_type); const itemUrl = item.url || '#'; // Add badge for activities showing upcoming count const isActivities = item.page_type === 'activities'; const showBadge = isActivities && upcomingCount > 0; return ( {item.label} {showBadge && ( {upcomingCount} )} ); })} {/* MyUIbrix Editor - Special item */} { e?.preventDefault(); window.open('/?myuibrix=edit', '_blank'); }} > MyUIbrix Editor ) : ( // Fallback to hardcoded navigation <> Hlavní Nástěnka {isAdmin && ( Analytika )} Obsah {/* Core sports entities first */} Týmy {/* Add subtle scroller hint */} Zápasy scroller Aktivity {upcomingCount > 0 && ( {upcomingCount} )} Hráči {/* Other content */} Články Kategorie O klubu Videa Galerie (Zonerama) Tabule (Scoreboard) Scoreboard Remote Oblečení Sponzoři Bannery Zprávy Kontakty Zpravodaj Ankety {isAdmin && ( <> Nastavení { e?.preventDefault(); window.open('/?myuibrix=edit', '_blank'); }} > MyUIbrix Editor Navigace Alias soutěží Prefetch & Cache Uživatelé Nastavení Soubory )} )} Odhlásit se ); }; export default AdminSidebar;