import React, { useState, useEffect } from 'react'; import { Box, Container, Heading, VStack, HStack, Button, Table, Thead, Tbody, Tr, Th, Td, IconButton, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, FormControl, FormLabel, Input, Select, Switch, useToast, Badge, Tabs, TabList, TabPanels, Tab, TabPanel, Spinner, Alert, AlertIcon, Tooltip, Text, Divider, Card, CardBody, useColorModeValue, Flex, Textarea, Collapse, Icon, } from '@chakra-ui/react'; import AdminLayout from '../../layouts/AdminLayout'; import { AddIcon, EditIcon, DeleteIcon, DragHandleIcon, ChevronUpIcon, ChevronDownIcon, ExternalLinkIcon, ChevronRightIcon, ViewIcon, ViewOffIcon, } from '@chakra-ui/icons'; import { FaFacebook, FaInstagram, FaYoutube, FaTwitter, FaTiktok, FaLinkedin, FaDiscord, FaTwitch, FaHome, FaInfoCircle, FaCalendarAlt, FaFutbol, FaUsers, FaTable, FaNewspaper, FaVideo, FaCamera, FaSearch, FaBars, FaCog, FaHandshake, FaEnvelope, FaUserShield, FaFolder, FaBook, FaTshirt, FaLink, FaPoll, } from 'react-icons/fa'; // Using simple up/down buttons instead of drag-drop for better compatibility import { NavigationItem, SocialLink, getAllNavigationItems, createNavigationItem, updateNavigationItem, deleteNavigationItem, reorderNavigationItems, getAllSocialLinks, createSocialLink, updateSocialLink, deleteSocialLink, reorderSocialLinks, seedDefaultNavigation, } from '../../services/navigation'; const PAGE_TYPE_OPTIONS = [ { value: 'home', label: 'Domů', url: '/' }, { value: 'about', label: 'O klubu', url: '/o-klubu' }, { value: 'calendar', label: 'Kalendář', url: '/kalendar' }, { value: 'matches', label: 'Zápasy', url: '/zapasy' }, { value: 'activities', label: 'Aktivity', url: '/aktivity' }, { value: 'players', label: 'Hráči', url: '/hraci' }, { value: 'tables', label: 'Tabulky', url: '/tabulky' }, { value: 'blog', label: 'Články', url: '/blog' }, { value: 'videos', label: 'Videa', url: '/videa' }, { value: 'gallery', label: 'Fotogalerie', url: '/galerie' }, { value: 'sponsors', label: 'Sponzoři', url: '/sponzori' }, { value: 'contact', label: 'Kontakt', url: '/kontakt' }, { value: 'search', label: 'Hledat', url: '/hledat' }, ]; const ADMIN_PAGE_PRESETS = [ { value: 'dashboard', label: 'Nástěnka', url: '/admin' }, { value: 'analytics', label: 'Analytika', url: '/admin/analytika' }, { value: 'teams', label: 'Týmy', url: '/admin/tymy' }, { value: 'matches', label: 'Zápasy', url: '/admin/zapasy' }, { value: 'activities', label: 'Aktivity', url: '/admin/aktivity' }, { value: 'players', label: 'Hráči', url: '/admin/hraci' }, { value: 'articles', label: 'Články', url: '/admin/clanky' }, { value: 'about', label: 'O klubu', url: '/admin/o-klubu' }, { value: 'videos', label: 'Videa', url: '/admin/videa' }, { value: 'gallery', label: 'Galerie', url: '/admin/galerie' }, { value: 'sponsors', label: 'Sponzoři', url: '/admin/sponzori' }, { value: 'messages', label: 'Zprávy', url: '/admin/zpravy' }, { value: 'contacts', label: 'Kontakty', url: '/admin/kontakty' }, { value: 'newsletter', label: 'Zpravodaj', url: '/admin/newsletter' }, { value: 'navigation', label: 'Navigace', url: '/admin/navigace' }, { value: 'users', label: 'Uživatelé', url: '/admin/uzivatele' }, { value: 'settings', label: 'Nastavení', url: '/admin/nastaveni' }, { value: 'files', label: 'Soubory', url: '/admin/soubory' }, { value: 'prefetch', label: 'Prefetch', url: '/admin/prefetch' }, { value: 'docs', label: 'Dokumentace', url: '/admin/docs' }, { value: 'webmail', label: 'Webmail', url: 'https://webmail.example.com' }, ]; const SOCIAL_PLATFORMS = [ { value: 'facebook', label: 'Facebook', icon: FaFacebook }, { value: 'instagram', label: 'Instagram', icon: FaInstagram }, { value: 'youtube', label: 'YouTube', icon: FaYoutube }, { value: 'twitter', label: 'Twitter', icon: FaTwitter }, { value: 'tiktok', label: 'TikTok', icon: FaTiktok }, { value: 'linkedin', label: 'LinkedIn', icon: FaLinkedin }, { value: 'discord', label: 'Discord', icon: FaDiscord }, { value: 'twitch', label: 'Twitch', icon: FaTwitch }, ]; const NAV_ICON_OPTIONS = [ { value: 'FaHome', label: 'Domů', icon: FaHome }, { value: 'FaInfoCircle', label: 'O klubu', icon: FaInfoCircle }, { value: 'FaCalendarAlt', label: 'Kalendář', icon: FaCalendarAlt }, { value: 'FaFutbol', label: 'Hráči', icon: FaFutbol }, { value: 'FaUsers', label: 'Týmy', icon: FaUsers }, { value: 'FaTable', label: 'Tabulky', icon: FaTable }, { value: 'FaNewspaper', label: 'Články', icon: FaNewspaper }, { value: 'FaVideo', label: 'Videa', icon: FaVideo }, { value: 'FaCamera', label: 'Galerie', icon: FaCamera }, { value: 'FaHandshake', label: 'Sponzoři', icon: FaHandshake }, { value: 'FaEnvelope', label: 'Kontakt', icon: FaEnvelope }, { value: 'FaSearch', label: 'Hledat', icon: FaSearch }, { value: 'FaBars', label: 'Menu', icon: FaBars }, { value: 'FaLink', label: 'Odkaz', icon: FaLink }, { value: 'FaCog', label: 'Nastavení', icon: FaCog }, { value: 'FaPoll', label: 'Ankety', icon: FaPoll }, { value: 'FaUserShield', label: 'Uživatelé', icon: FaUserShield }, { value: 'FaFolder', label: 'Soubory', icon: FaFolder }, { value: 'FaBook', label: 'Stránka', icon: FaBook }, { value: 'FaTshirt', label: 'Oblečení', icon: FaTshirt }, ]; const ICON_COMPONENTS: Record = Object.fromEntries(NAV_ICON_OPTIONS.map(opt => [opt.value, opt.icon])); // NavItemCard component for hierarchical display interface NavItemCardProps { item: NavigationItem; index: number; total: number; onMoveUp: () => void; onMoveDown: () => void; onEdit: () => void; onDelete: () => void; onAddChild: () => void; isExpanded: boolean; onToggleExpand: () => void; cardBg: string; borderColor: string; hoverBg: string; level?: number; onChildMoveUp?: (parentId: number, index: number) => void; onChildMoveDown?: (parentId: number, index: number) => void; } const NavItemCard: React.FC = ({ item, index, total, onMoveUp, onMoveDown, onEdit, onDelete, onAddChild, isExpanded, onToggleExpand, cardBg, borderColor, hoverBg, level = 0, onChildMoveUp, onChildMoveDown, }) => { const hasChildren = item.children && item.children.length > 0; const indentPx = level * 32; return ( {/* Expand/Collapse button for items with children */} {hasChildren ? ( : } size="sm" variant="ghost" onClick={onToggleExpand} /> ) : ( )} {/* Reorder buttons */} } size="xs" isDisabled={index === 0} onClick={onMoveUp} variant="ghost" /> } size="xs" isDisabled={index === total - 1} onClick={onMoveDown} variant="ghost" /> {/* Item info */} {item.label} {item.type} {!item.visible && ( Skryto )} {item.url && ( {item.url} {item.type === 'external' && } )} {item.page_type && !item.url && ( Page: {item.page_type} )} {hasChildren && ( {item.children!.length} {item.children!.length === 1 ? 'podpoložka' : 'podpoložek'} )} {/* Action buttons */} {item.type === 'dropdown' && ( } size="sm" colorScheme="green" variant="ghost" onClick={onAddChild} /> )} } size="sm" variant="ghost" onClick={onEdit} /> } size="sm" colorScheme="red" variant="ghost" onClick={onDelete} /> {/* Render children if expanded */} {hasChildren && isExpanded && ( {item.children!.map((child, childIndex) => ( onChildMoveUp && onChildMoveUp(item.id!, childIndex)} onMoveDown={() => onChildMoveDown && onChildMoveDown(item.id!, childIndex)} onEdit={() => onEdit()} onDelete={() => onDelete()} onAddChild={() => {}} isExpanded={false} onToggleExpand={() => {}} cardBg={cardBg} borderColor={borderColor} hoverBg={hoverBg} level={level + 1} onChildMoveUp={onChildMoveUp} onChildMoveDown={onChildMoveDown} /> ))} )} ); }; const NavigationAdminPage = () => { const [navItems, setNavItems] = useState([]); const [adminNavItems, setAdminNavItems] = useState([]); const [socialLinks, setSocialLinks] = useState([]); const [loading, setLoading] = useState(true); const [editingNav, setEditingNav] = useState(null); const [editingSocial, setEditingSocial] = useState(null); const [expandedItems, setExpandedItems] = useState>(new Set()); const [isAdminNav, setIsAdminNav] = useState(false); const toast = useToast(); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.600'); const hoverBg = useColorModeValue('gray.50', 'gray.700'); const { isOpen: isNavModalOpen, onOpen: onNavModalOpen, onClose: onNavModalClose } = useDisclosure(); const { isOpen: isSocialModalOpen, onOpen: onSocialModalOpen, onClose: onSocialModalClose } = useDisclosure(); useEffect(() => { loadData(); }, []); const loadData = async () => { try { setLoading(true); const [navData, socialData] = await Promise.all([ getAllNavigationItems(), getAllSocialLinks(), ]); // Auto-seed if navigation is empty if (!navData || navData.length === 0) { try { const seedResult = await seedDefaultNavigation(); if (seedResult.seeded) { toast({ title: 'Výchozí navigace vytvořena', description: `Automaticky vytvořeno ${seedResult.count} položek navigace`, status: 'success', duration: 4000, isClosable: true, }); // Reload data after seeding const [newNavData, newSocialData] = await Promise.all([ getAllNavigationItems(), getAllSocialLinks(), ]); const frontend = (newNavData || []).filter(item => !item.requires_admin); const admin = (newNavData || []).filter(item => item.requires_admin); setNavItems(frontend); setAdminNavItems(admin); setSocialLinks(newSocialData || []); return; } } catch (seedError) { console.error('Chyba při automatickém seedování:', seedError); // Continue with empty navigation } } // Split nav items into frontend and admin const frontend = (navData || []).filter(item => !item.requires_admin); const admin = (navData || []).filter(item => item.requires_admin); setNavItems(frontend); setAdminNavItems(admin); setSocialLinks(socialData || []); } catch (error: any) { console.error('Chyba při načítání navigace:', error); toast({ title: 'Chyba při načítání dat', description: error?.response?.data?.error || error?.message || 'Neznámá chyba', status: 'error', duration: 5000, isClosable: true, }); } finally { setLoading(false); } }; const moveChildNavItem = async (parentId: number, index: number, direction: 'up' | 'down') => { const moveWithin = async ( list: NavigationItem[], setList: React.Dispatch> ): Promise => { const parentIdx = list.findIndex((it) => it.id === parentId); if (parentIdx === -1) return false; const parent = list[parentIdx]; const children = Array.isArray(parent.children) ? [...parent.children] : []; if (children.length === 0) return true; if (direction === 'up' && index === 0) return true; if (direction === 'down' && index === children.length - 1) return true; const targetIndex = direction === 'up' ? index - 1 : index + 1; [children[index], children[targetIndex]] = [children[targetIndex], children[index]]; const updatedParent: NavigationItem = { ...parent, children }; const updated = [...list]; updated[parentIdx] = updatedParent; setList(updated); const orders = children.map((c, idx) => ({ id: c.id!, display_order: idx })); try { await reorderNavigationItems(orders); toast({ title: 'Pořadí aktualizováno', status: 'success', duration: 2000 }); } catch (err) { toast({ title: 'Chyba při aktualizaci pořadí', status: 'error', duration: 3000 }); loadData(); } return true; }; const doneFront = await moveWithin(navItems, setNavItems); if (!doneFront) { await moveWithin(adminNavItems, setAdminNavItems); } }; const moveNavItem = async (index: number, direction: 'up' | 'down') => { if (direction === 'up' && index === 0) return; if (direction === 'down' && index === navItems.length - 1) return; const items = Array.from(navItems); const targetIndex = direction === 'up' ? index - 1 : index + 1; [items[index], items[targetIndex]] = [items[targetIndex], items[index]]; setNavItems(items); const orders = items.map((item, idx) => ({ id: item.id!, display_order: idx, })); try { await reorderNavigationItems(orders); toast({ title: 'Pořadí aktualizováno', status: 'success', duration: 2000, }); } catch (error) { toast({ title: 'Chyba při aktualizaci pořadí', status: 'error', duration: 3000, }); loadData(); } }; const moveAdminNavItem = async (index: number, direction: 'up' | 'down') => { if (direction === 'up' && index === 0) return; if (direction === 'down' && index === adminNavItems.length - 1) return; const items = Array.from(adminNavItems); const targetIndex = direction === 'up' ? index - 1 : index + 1; [items[index], items[targetIndex]] = [items[targetIndex], items[index]]; setAdminNavItems(items); const orders = items.map((item, idx) => ({ id: item.id!, display_order: idx, })); try { await reorderNavigationItems(orders); toast({ title: 'Pořadí aktualizováno', status: 'success', duration: 2000, }); } catch (error) { toast({ title: 'Chyba při aktualizaci pořadí', status: 'error', duration: 3000, }); loadData(); } }; const moveSocialLink = async (index: number, direction: 'up' | 'down') => { if (direction === 'up' && index === 0) return; if (direction === 'down' && index === socialLinks.length - 1) return; const items = Array.from(socialLinks); const targetIndex = direction === 'up' ? index - 1 : index + 1; [items[index], items[targetIndex]] = [items[targetIndex], items[index]]; setSocialLinks(items); const orders = items.map((item, idx) => ({ id: item.id!, display_order: idx, })); try { await reorderSocialLinks(orders); toast({ title: 'Pořadí aktualizováno', status: 'success', duration: 2000, }); } catch (error) { toast({ title: 'Chyba při aktualizaci pořadí', status: 'error', duration: 3000, }); loadData(); } }; const openNavModal = (item?: NavigationItem, parentId?: number, forAdmin?: boolean) => { setIsAdminNav(forAdmin || false); if (item) { setEditingNav(item); } else { const itemsCount = forAdmin ? adminNavItems.length : navItems.length; setEditingNav({ label: '', type: forAdmin ? 'internal' : 'page', visible: true, display_order: itemsCount, target: '_self', parent_id: parentId, requires_admin: forAdmin || false, } as NavigationItem); } onNavModalOpen(); }; const toggleExpand = (id: number) => { setExpandedItems(prev => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const openSocialModal = (link?: SocialLink) => { if (link) { setEditingSocial(link); } else { setEditingSocial({ platform: 'facebook', url: '', visible: true, display_order: socialLinks.length, } as SocialLink); } onSocialModalOpen(); }; const saveNavItem = async () => { if (!editingNav) return; try { if (editingNav.id) { await updateNavigationItem(editingNav.id, editingNav); toast({ title: 'Položka aktualizována', status: 'success', duration: 2000, }); } else { await createNavigationItem(editingNav); toast({ title: 'Položka vytvořena', status: 'success', duration: 2000, }); } onNavModalClose(); loadData(); } catch (error) { toast({ title: 'Chyba při ukládání', status: 'error', duration: 3000, }); } }; const saveSocialLink = async () => { if (!editingSocial) return; try { if (editingSocial.id) { await updateSocialLink(editingSocial.id, editingSocial); toast({ title: 'Odkaz aktualizován', status: 'success', duration: 2000, }); } else { await createSocialLink(editingSocial); toast({ title: 'Odkaz vytvořen', status: 'success', duration: 2000, }); } onSocialModalClose(); loadData(); } catch (error) { toast({ title: 'Chyba při ukládání', status: 'error', duration: 3000, }); } }; const deleteNav = async (id: number) => { if (!window.confirm('Opravdu smazat tuto položku?')) return; try { await deleteNavigationItem(id); toast({ title: 'Položka smazána', status: 'success', duration: 2000, }); loadData(); } catch (error) { toast({ title: 'Chyba při mazání', status: 'error', duration: 3000, }); } }; const deleteSocial = async (id: number) => { if (!window.confirm('Opravdu smazat tento odkaz?')) return; try { await deleteSocialLink(id); toast({ title: 'Odkaz smazán', status: 'success', duration: 2000, }); loadData(); } catch (error) { toast({ title: 'Chyba při mazání', status: 'error', duration: 3000, }); } }; const getSocialIcon = (platform: string) => { const found = SOCIAL_PLATFORMS.find(p => p.value === platform); return found?.icon || FaFacebook; }; const handleSeedDefaultNavigation = async () => { if (!window.confirm('Vytvořit výchozí navigační položky? Toto lze provést pouze pokud databáze je prázdná.')) return; try { const result = await seedDefaultNavigation(); if (result.seeded) { toast({ title: 'Výchozí navigace vytvořena', description: `Vytvořeno ${result.count} položek`, status: 'success', duration: 3000, isClosable: true, }); loadData(); } else { toast({ title: 'Navigace již existuje', description: result.message, status: 'info', duration: 3000, isClosable: true, }); } } catch (error: any) { console.error('Chyba při seedování navigace:', error); toast({ title: 'Chyba při vytváření výchozí navigace', description: error?.response?.data?.error || error?.message || 'Neznámá chyba', status: 'error', duration: 5000, isClosable: true, }); } }; if (loading) { return ( Načítání... ); } return ( Správa navigace Načteno: {navItems.length} webových, {adminNavItems.length} admin Oddělená správa navigace Webová navigace: Menu na veřejném webu
Admin panel: Postranní menu v administraci
Webová navigace Admin panel {/* Frontend Navigation Items Tab */} Přetahujte položky nahoru/dolů pro změnu pořadí. Klikněte na šipku pro zobrazení podpoložek. Tipy pro správu navigace: • Použijte typ "Dropdown" pro položky s podpoložkami
• Podpoložky se zobrazí při najetí myší na hlavní položku
• Položky můžete skrýt bez smazání pomocí přepínače viditelnosti
{navItems.length === 0 ? ( Žádné položky navigace Nebyly nalezeny žádné položky navigace. Můžete vytvořit výchozí navigaci nebo přidat položky ručně. ) : ( navItems.map((item, index) => ( moveNavItem(index, 'up')} onMoveDown={() => moveNavItem(index, 'down')} onEdit={() => openNavModal(item)} onDelete={() => deleteNav(item.id!)} onAddChild={() => openNavModal(undefined, item.id)} isExpanded={expandedItems.has(item.id!)} onToggleExpand={() => toggleExpand(item.id!)} cardBg={cardBg} borderColor={borderColor} hoverBg={hoverBg} onChildMoveUp={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'up')} onChildMoveDown={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'down')} /> )) )}
{/* Admin Panel Navigation Tab */} Správa bočního menu v administraci Správa admin panelu: • Vyberte z přednastavených stránek nebo přidejte vlastní
• Skryjte nepotřebné sekce pomocí viditelnosti
• Přidejte externí odkazy (např. Webmail)
• Kategorizujte pomocí dropdown menu
{adminNavItems.map((item, index) => ( moveAdminNavItem(index, 'up')} onMoveDown={() => moveAdminNavItem(index, 'down')} onEdit={() => openNavModal(item, undefined, true)} onDelete={() => deleteNav(item.id!)} onAddChild={() => openNavModal(undefined, item.id, true)} isExpanded={expandedItems.has(item.id!)} onToggleExpand={() => toggleExpand(item.id!)} cardBg={cardBg} borderColor={borderColor} hoverBg={hoverBg} onChildMoveUp={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'up')} onChildMoveDown={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'down')} /> ))} {adminNavItems.length === 0 && ( Žádné vlastní položky v admin panelu. Použijte tlačítko "Přidat položku" pro vytvoření vlastní navigace. )}
{/* Navigation Item Modal */} {editingNav?.id ? 'Upravit položku' : 'Nová položka'} {isAdminNav && Admin panel} {isAdminNav && !editingNav?.id && ( Vytvářte položku pro boční menu v administraci. Můžete vybrat přednastavené stránky nebo přidat vlastní odkazy. )} Název setEditingNav({ ...editingNav!, label: e.target.value })} placeholder={isAdminNav ? "Např. Nástěnka, Webmail" : "Např. Domů, O klubu"} /> Typ {editingNav?.type === 'page' && !isAdminNav && ( Webová stránka )} {editingNav?.type === 'internal' && isAdminNav && ( Admin stránka )} {editingNav?.type === 'dropdown' && ( Po vytvoření této položky můžete přidat podpoložky kliknutím na tlačítko "Přidat podpoložku". )} {(editingNav?.type === 'internal' || editingNav?.type === 'external') && ( URL setEditingNav({ ...editingNav!, url: e.target.value })} placeholder={ editingNav?.type === 'external' ? 'https://example.com' : '/vlastni-stranka' } /> )} Ikona {editingNav?.icon && ( {editingNav.icon} )} {editingNav?.parent_id && ( Toto je podpoložka. Zobrazí se v dropdown menu rodičovské položky. )} Popis (volitelné)