This commit is contained in:
Tomas Dvorak
2025-10-29 21:20:16 +01:00
parent 823fabee02
commit 16e4533202
61 changed files with 2308 additions and 942 deletions
+119 -18
View File
@@ -45,6 +45,7 @@ import {
Flex,
Textarea,
Collapse,
Icon,
} from '@chakra-ui/react';
import AdminLayout from '../../layouts/AdminLayout';
import {
@@ -68,6 +69,26 @@ import {
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 {
@@ -137,6 +158,31 @@ const SOCIAL_PLATFORMS = [
{ 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;
@@ -153,6 +199,8 @@ interface NavItemCardProps {
borderColor: string;
hoverBg: string;
level?: number;
onChildMoveUp?: (parentId: number, index: number) => void;
onChildMoveDown?: (parentId: number, index: number) => void;
}
const NavItemCard: React.FC<NavItemCardProps> = ({
@@ -170,6 +218,8 @@ const NavItemCard: React.FC<NavItemCardProps> = ({
borderColor,
hoverBg,
level = 0,
onChildMoveUp,
onChildMoveDown,
}) => {
const hasChildren = item.children && item.children.length > 0;
const indentPx = level * 32;
@@ -299,14 +349,14 @@ const NavItemCard: React.FC<NavItemCardProps> = ({
{/* Render children if expanded */}
{hasChildren && isExpanded && (
<VStack spacing={2} align="stretch" mt={2}>
{item.children!.map((child) => (
{item.children!.map((child, childIndex) => (
<NavItemCard
key={child.id}
item={child}
index={0}
total={1}
onMoveUp={() => {}}
onMoveDown={() => {}}
index={childIndex}
total={item.children!.length}
onMoveUp={() => onChildMoveUp && onChildMoveUp(item.id!, childIndex)}
onMoveDown={() => onChildMoveDown && onChildMoveDown(item.id!, childIndex)}
onEdit={() => onEdit()}
onDelete={() => onDelete()}
onAddChild={() => {}}
@@ -316,6 +366,8 @@ const NavItemCard: React.FC<NavItemCardProps> = ({
borderColor={borderColor}
hoverBg={hoverBg}
level={level + 1}
onChildMoveUp={onChildMoveUp}
onChildMoveDown={onChildMoveDown}
/>
))}
</VStack>
@@ -352,13 +404,9 @@ const NavigationAdminPage = () => {
getAllNavigationItems(),
getAllSocialLinks(),
]);
console.log('Načtená navigace:', navData);
console.log('Načtené sociální odkazy:', socialData);
// Auto-seed if navigation is empty
if (!navData || navData.length === 0) {
console.log('Navigace je prázdná, automaticky vytváříme výchozí navigaci...');
try {
const seedResult = await seedDefaultNavigation();
if (seedResult.seeded) {
@@ -408,6 +456,43 @@ const NavigationAdminPage = () => {
}
};
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;
@@ -811,6 +896,8 @@ const NavigationAdminPage = () => {
cardBg={cardBg}
borderColor={borderColor}
hoverBg={hoverBg}
onChildMoveUp={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'up')}
onChildMoveDown={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'down')}
/>
))
)}
@@ -860,6 +947,8 @@ const NavigationAdminPage = () => {
cardBg={cardBg}
borderColor={borderColor}
hoverBg={hoverBg}
onChildMoveUp={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'up')}
onChildMoveDown={(parentId, childIdx) => moveChildNavItem(parentId, childIdx, 'down')}
/>
))}
@@ -1026,6 +1115,25 @@ const NavigationAdminPage = () => {
</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 />
@@ -1043,14 +1151,7 @@ const NavigationAdminPage = () => {
/>
</FormControl>
<FormControl>
<FormLabel>CSS třída (volitelné)</FormLabel>
<Input
value={editingNav?.icon || ''}
onChange={(e) => setEditingNav({ ...editingNav!, icon: e.target.value })}
placeholder="custom-class"
/>
</FormControl>
{editingNav?.type === 'external' && (
<FormControl>