Files
MyClub/frontend/src/pages/admin/NavigationAdminPage.tsx
T
Tomas Dvorak 16e4533202 dev day #75
2025-10-29 21:20:16 +01:00

1198 lines
41 KiB
TypeScript

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<string, any> = 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<NavItemCardProps> = ({
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 (
<Box ml={`${indentPx}px`}>
<Card
bg={item.visible ? cardBg : useColorModeValue('gray.100', 'gray.700')}
borderWidth="1px"
borderColor={borderColor}
_hover={{ bg: hoverBg }}
transition="all 0.2s"
>
<CardBody py={3}>
<Flex align="center" gap={3}>
{/* Expand/Collapse button for items with children */}
{hasChildren ? (
<IconButton
aria-label="Toggle children"
icon={isExpanded ? <ChevronDownIcon /> : <ChevronRightIcon />}
size="sm"
variant="ghost"
onClick={onToggleExpand}
/>
) : (
<Box w="32px" />
)}
{/* Reorder buttons */}
<VStack spacing={0}>
<IconButton
aria-label="Nahoru"
icon={<ChevronUpIcon />}
size="xs"
isDisabled={index === 0}
onClick={onMoveUp}
variant="ghost"
/>
<IconButton
aria-label="Dolů"
icon={<ChevronDownIcon />}
size="xs"
isDisabled={index === total - 1}
onClick={onMoveDown}
variant="ghost"
/>
</VStack>
{/* Item info */}
<VStack align="start" flex={1} spacing={1}>
<HStack>
<Text fontWeight="bold" fontSize="md">
{item.label}
</Text>
<Badge colorScheme={
item.type === 'external' ? 'orange' :
item.type === 'dropdown' ? 'purple' :
item.type === 'page' ? 'blue' : 'green'
}>
{item.type}
</Badge>
{!item.visible && (
<Badge colorScheme="red">
<HStack spacing={1}>
<ViewOffIcon />
<Text>Skryto</Text>
</HStack>
</Badge>
)}
</HStack>
<HStack spacing={2} fontSize="sm" color="gray.500">
{item.url && (
<HStack>
<Text isTruncated maxW="300px">{item.url}</Text>
{item.type === 'external' && <ExternalLinkIcon />}
</HStack>
)}
{item.page_type && !item.url && (
<Text>Page: {item.page_type}</Text>
)}
{hasChildren && (
<Badge colorScheme="cyan">
{item.children!.length} {item.children!.length === 1 ? 'podpoložka' : 'podpoložek'}
</Badge>
)}
</HStack>
</VStack>
{/* Action buttons */}
<HStack spacing={1}>
{item.type === 'dropdown' && (
<Tooltip label="Přidat podpoložku">
<IconButton
aria-label="Přidat podpoložku"
icon={<AddIcon />}
size="sm"
colorScheme="green"
variant="ghost"
onClick={onAddChild}
/>
</Tooltip>
)}
<Tooltip label="Upravit">
<IconButton
aria-label="Upravit"
icon={<EditIcon />}
size="sm"
variant="ghost"
onClick={onEdit}
/>
</Tooltip>
<Tooltip label="Smazat">
<IconButton
aria-label="Smazat"
icon={<DeleteIcon />}
size="sm"
colorScheme="red"
variant="ghost"
onClick={onDelete}
/>
</Tooltip>
</HStack>
</Flex>
</CardBody>
</Card>
{/* Render children if expanded */}
{hasChildren && isExpanded && (
<VStack spacing={2} align="stretch" mt={2}>
{item.children!.map((child, childIndex) => (
<NavItemCard
key={child.id}
item={child}
index={childIndex}
total={item.children!.length}
onMoveUp={() => 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}
/>
))}
</VStack>
)}
</Box>
);
};
const NavigationAdminPage = () => {
const [navItems, setNavItems] = useState<NavigationItem[]>([]);
const [adminNavItems, setAdminNavItems] = useState<NavigationItem[]>([]);
const [socialLinks, setSocialLinks] = useState<SocialLink[]>([]);
const [loading, setLoading] = useState(true);
const [editingNav, setEditingNav] = useState<NavigationItem | null>(null);
const [editingSocial, setEditingSocial] = useState<SocialLink | null>(null);
const [expandedItems, setExpandedItems] = useState<Set<number>>(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<React.SetStateAction<NavigationItem[]>>
): Promise<boolean> => {
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 (
<AdminLayout>
<Container maxW="7xl" py={8}>
<VStack spacing={4}>
<Spinner size="xl" />
<Text>Načítání...</Text>
</VStack>
</Container>
</AdminLayout>
);
}
return (
<AdminLayout>
<Container maxW="7xl" py={8}>
<VStack spacing={6} align="stretch">
<HStack justify="space-between" align="center">
<Heading size="lg">Správa navigace</Heading>
<Button onClick={loadData} size="sm" colorScheme="gray">
Obnovit data
</Button>
</HStack>
<Alert status="info" variant="left-accent">
<AlertIcon />
<Box flex="1">
<HStack spacing={4}>
<Text fontSize="sm">
<strong>Načteno:</strong> {navItems.length} webových, {adminNavItems.length} admin
</Text>
</HStack>
</Box>
</Alert>
<Alert status="info">
<AlertIcon />
<Box>
<Text fontWeight="bold">Oddělená správa navigace</Text>
<Text fontSize="sm" mt={1}>
<strong>Webová navigace:</strong> Menu na veřejném webu<br/>
<strong>Admin panel:</strong> Postranní menu v administraci
</Text>
</Box>
</Alert>
<Tabs>
<TabList>
<Tab>Webová navigace</Tab>
<Tab>Admin panel</Tab>
</TabList>
<TabPanels>
{/* Frontend Navigation Items Tab */}
<TabPanel>
<VStack spacing={4} align="stretch">
<HStack>
<Button leftIcon={<AddIcon />} colorScheme="blue" onClick={() => openNavModal()}>
Přidat hlavní položku
</Button>
<Text fontSize="sm" color="gray.500">
Přetahujte položky nahoru/dolů pro změnu pořadí. Klikněte na šipku pro zobrazení podpoložek.
</Text>
</HStack>
<Alert status="info" borderRadius="md">
<AlertIcon />
<Box>
<Text fontWeight="bold">Tipy pro správu navigace:</Text>
<Text fontSize="sm" mt={1}>
Použijte typ "Dropdown" pro položky s podpoložkami<br/>
Podpoložky se zobrazí při najetí myší na hlavní položku<br/>
Položky můžete skrýt bez smazání pomocí přepínače viditelnosti
</Text>
</Box>
</Alert>
<VStack spacing={2} align="stretch">
{navItems.length === 0 ? (
<Alert status="warning" variant="left-accent">
<AlertIcon />
<Box flex="1">
<Text fontWeight="bold">Žádné položky navigace</Text>
<Text fontSize="sm" mt={1} mb={2}>
Nebyly nalezeny žádné položky navigace. Můžete vytvořit výchozí navigaci nebo přidat položky ručně.
</Text>
<HStack spacing={2}>
<Button
size="sm"
colorScheme="blue"
onClick={handleSeedDefaultNavigation}
>
Vytvořit výchozí navigaci
</Button>
<Button
size="sm"
variant="outline"
onClick={() => openNavModal()}
>
Přidat položku ručně
</Button>
</HStack>
</Box>
</Alert>
) : (
navItems.map((item, index) => (
<NavItemCard
key={item.id}
item={item}
index={index}
total={navItems.length}
onMoveUp={() => 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')}
/>
))
)}
</VStack>
</VStack>
</TabPanel>
{/* Admin Panel Navigation Tab */}
<TabPanel>
<VStack spacing={4} align="stretch">
<HStack>
<Button leftIcon={<AddIcon />} colorScheme="purple" onClick={() => openNavModal(undefined, undefined, true)}>
Přidat položku do admin panelu
</Button>
<Text fontSize="sm" color="gray.500">
Správa bočního menu v administraci
</Text>
</HStack>
<Alert status="info" borderRadius="md" colorScheme="purple">
<AlertIcon />
<Box>
<Text fontWeight="bold">Správa admin panelu:</Text>
<Text fontSize="sm" mt={1}>
Vyberte z přednastavených stránek nebo přidejte vlastní<br/>
Skryjte nepotřebné sekce pomocí viditelnosti<br/>
Přidejte externí odkazy (např. Webmail)<br/>
Kategorizujte pomocí dropdown menu
</Text>
</Box>
</Alert>
<VStack spacing={2} align="stretch">
{adminNavItems.map((item, index) => (
<NavItemCard
key={item.id}
item={item}
index={index}
total={adminNavItems.length}
onMoveUp={() => 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 && (
<Alert status="warning">
<AlertIcon />
Žádné vlastní položky v admin panelu. Použijte tlačítko "Přidat položku" pro vytvoření vlastní navigace.
</Alert>
)}
</VStack>
</VStack>
</TabPanel>
</TabPanels>
</Tabs>
</VStack>
{/* Navigation Item Modal */}
<Modal isOpen={isNavModalOpen} onClose={onNavModalClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
{editingNav?.id ? 'Upravit položku' : 'Nová položka'}
{isAdminNav && <Badge ml={2} colorScheme="purple">Admin panel</Badge>}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
{isAdminNav && !editingNav?.id && (
<Alert status="info" fontSize="sm">
<AlertIcon />
Vytvářte položku pro boční menu v administraci. Můžete vybrat přednastavené stránky nebo přidat vlastní odkazy.
</Alert>
)}
<FormControl isRequired>
<FormLabel>Název</FormLabel>
<Input
value={editingNav?.label || ''}
onChange={(e) => setEditingNav({ ...editingNav!, label: e.target.value })}
placeholder={isAdminNav ? "Např. Nástěnka, Webmail" : "Např. Domů, O klubu"}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>Typ</FormLabel>
<Select
value={editingNav?.type || (isAdminNav ? 'internal' : 'page')}
onChange={(e) =>
setEditingNav({ ...editingNav!, type: e.target.value as any })
}
>
{isAdminNav ? (
<>
<option value="internal">Admin stránka (vyberte existující)</option>
<option value="external">Externí odkaz (např. Webmail)</option>
<option value="dropdown">Kategorie ( podpoložky)</option>
</>
) : (
<>
<option value="page">Stránka (vyberte existující)</option>
<option value="internal">Interní odkaz (vlastní URL)</option>
<option value="external">Externí odkaz</option>
<option value="dropdown">Dropdown ( podpoložky)</option>
</>
)}
</Select>
</FormControl>
{editingNav?.type === 'page' && !isAdminNav && (
<FormControl isRequired>
<FormLabel>Webová stránka</FormLabel>
<Select
value={editingNav?.page_type || ''}
onChange={(e) => {
const selected = PAGE_TYPE_OPTIONS.find(opt => opt.value === e.target.value);
setEditingNav({
...editingNav!,
page_type: e.target.value,
url: selected?.url || '',
label: editingNav?.label || selected?.label || ''
});
}}
>
<option value="">-- Vyberte stránku --</option>
{PAGE_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label} ({opt.url})
</option>
))}
</Select>
</FormControl>
)}
{editingNav?.type === 'internal' && isAdminNav && (
<FormControl isRequired>
<FormLabel>Admin stránka</FormLabel>
<Select
value={editingNav?.page_type || ''}
onChange={(e) => {
const selected = ADMIN_PAGE_PRESETS.find(opt => opt.value === e.target.value);
const isExternal = selected?.url?.startsWith('http');
setEditingNav({
...editingNav!,
page_type: e.target.value,
url: selected?.url || '',
label: editingNav?.label || selected?.label || '',
type: isExternal ? 'external' : 'internal',
});
}}
>
<option value="">-- Vyberte admin stránku --</option>
<optgroup label="Hlavní">
<option value="dashboard">Nástěnka (/admin)</option>
<option value="analytics">Analytika (/admin/analytika)</option>
</optgroup>
<optgroup label="Obsah">
<option value="teams">Týmy (/admin/tymy)</option>
<option value="matches">Zápasy (/admin/zapasy)</option>
<option value="activities">Aktivity (/admin/aktivity)</option>
<option value="players">Hráči (/admin/hraci)</option>
<option value="articles">Články (/admin/clanky)</option>
<option value="videos">Videa (/admin/videa)</option>
<option value="gallery">Galerie (/admin/galerie)</option>
</optgroup>
<optgroup label="Komunikace">
<option value="messages">Zprávy (/admin/zpravy)</option>
<option value="contacts">Kontakty (/admin/kontakty)</option>
<option value="newsletter">Zpravodaj (/admin/newsletter)</option>
</optgroup>
<optgroup label="Nastavení">
<option value="navigation">Navigace (/admin/navigace)</option>
<option value="users">Uživatelé (/admin/uzivatele)</option>
<option value="settings">Nastavení (/admin/nastaveni)</option>
<option value="files">Soubory (/admin/soubory)</option>
<option value="prefetch">Prefetch (/admin/prefetch)</option>
<option value="docs">Dokumentace (/admin/docs)</option>
</optgroup>
<optgroup label="Externí odkazy">
<option value="webmail">Webmail (vlastní URL)</option>
</optgroup>
</Select>
</FormControl>
)}
{editingNav?.type === 'dropdown' && (
<Alert status="info" fontSize="sm">
<AlertIcon />
Po vytvoření této položky můžete přidat podpoložky kliknutím na tlačítko "Přidat podpoložku".
</Alert>
)}
{(editingNav?.type === 'internal' || editingNav?.type === 'external') && (
<FormControl isRequired>
<FormLabel>URL</FormLabel>
<Input
value={editingNav?.url || ''}
onChange={(e) => setEditingNav({ ...editingNav!, url: e.target.value })}
placeholder={
editingNav?.type === 'external'
? 'https://example.com'
: '/vlastni-stranka'
}
/>
</FormControl>
)}
<FormControl>
<FormLabel>Ikona</FormLabel>
<Select
value={editingNav?.icon || ''}
onChange={(e) => setEditingNav({ ...editingNav!, icon: e.target.value || undefined })}
>
<option value="">Bez ikony</option>
{NAV_ICON_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</Select>
{editingNav?.icon && (
<HStack mt={2} spacing={2} align="center">
<Icon as={ICON_COMPONENTS[editingNav.icon]} boxSize={5} />
<Text fontSize="sm">{editingNav.icon}</Text>
</HStack>
)}
</FormControl>
{editingNav?.parent_id && (
<Alert status="warning" fontSize="sm">
<AlertIcon />
Toto je podpoložka. Zobrazí se v dropdown menu rodičovské položky.
</Alert>
)}
<FormControl>
<FormLabel>Popis (volitelné)</FormLabel>
<Textarea
value={editingNav?.css_class || ''}
onChange={(e) => setEditingNav({ ...editingNav!, css_class: e.target.value })}
placeholder="Krátký popis pro administrátory"
rows={2}
/>
</FormControl>
{editingNav?.type === 'external' && (
<FormControl>
<FormLabel>Target</FormLabel>
<Select
value={editingNav?.target || '_self'}
onChange={(e) =>
setEditingNav({ ...editingNav!, target: e.target.value as any })
}
>
<option value="_self">Stejné okno</option>
<option value="_blank">Nové okno</option>
</Select>
</FormControl>
)}
<FormControl display="flex" alignItems="center">
<FormLabel mb="0">Viditelné</FormLabel>
<Switch
isChecked={editingNav?.visible ?? true}
onChange={(e) =>
setEditingNav({ ...editingNav!, visible: e.target.checked })
}
/>
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onNavModalClose}>
Zrušit
</Button>
<Button colorScheme="blue" onClick={saveNavItem}>
Uložit
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Container>
</AdminLayout>
);
};
export default NavigationAdminPage;