mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
1198 lines
41 KiB
TypeScript
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 (má 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 (má 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;
|