mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
634 lines
18 KiB
TypeScript
634 lines
18 KiB
TypeScript
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';
|
|
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
|
|
|
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 (
|
|
<ChakraLink
|
|
as={LinkComponent}
|
|
{...linkProps}
|
|
display="flex"
|
|
alignItems="center"
|
|
px={3}
|
|
py={2.5}
|
|
borderRadius="lg"
|
|
bg={isActive ? activeBg : 'transparent'}
|
|
color={isActive ? activeColor : 'inherit'}
|
|
fontWeight={isActive ? 'semibold' : 'medium'}
|
|
fontSize="sm"
|
|
_hover={{
|
|
textDecoration: 'none',
|
|
bg: isActive ? activeBg : useColorModeValue('gray.100', 'gray.700'),
|
|
transform: 'translateX(2px)',
|
|
}}
|
|
transition="all 0.2s ease"
|
|
onClick={handleClick}
|
|
data-navitem="true"
|
|
data-active={isActive ? 'true' : undefined}
|
|
position="relative"
|
|
_before={isActive ? {
|
|
content: '""',
|
|
position: 'absolute',
|
|
left: 0,
|
|
top: '50%',
|
|
transform: 'translateY(-50%)',
|
|
width: '3px',
|
|
height: '60%',
|
|
bg: activeColor,
|
|
borderRadius: 'full',
|
|
} : {}}
|
|
>
|
|
<Icon as={icon} mr={3} boxSize={4} />
|
|
<Text flex={1}>{children}</Text>
|
|
</ChakraLink>
|
|
);
|
|
};
|
|
|
|
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<string, any> = {
|
|
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 { data: publicSettings } = usePublicSettings();
|
|
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<HTMLDivElement | null>(null);
|
|
const location = useLocation();
|
|
const STORAGE_KEY = 'admin-sidebar-scroll';
|
|
|
|
// Dynamic navigation state
|
|
const [navItems, setNavItems] = useState<NavigationItem[]>([]);
|
|
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 (
|
|
<Box
|
|
as="nav"
|
|
position="fixed"
|
|
left={0}
|
|
top={0}
|
|
bottom={0}
|
|
width="260px"
|
|
bg={bg}
|
|
borderRightWidth={borderRight}
|
|
borderColor={borderColor}
|
|
pt={5}
|
|
display={{ base: isOpen ? 'block' : 'none', md: 'block' }}
|
|
zIndex={10}
|
|
overflowY="auto"
|
|
overflowX="hidden"
|
|
boxShadow="lg"
|
|
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
|
ref={scrollRef}
|
|
onScroll={handleScroll}
|
|
css={{
|
|
'&::-webkit-scrollbar': { width: '4px' },
|
|
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
|
'&::-webkit-scrollbar-thumb': { background: useColorModeValue('gray.300', 'gray.600'), borderRadius: '2px' },
|
|
'&::-webkit-scrollbar-thumb:hover': { background: useColorModeValue('gray.400', 'gray.500') },
|
|
}}
|
|
>
|
|
<VStack align="stretch" spacing={1} px={3} pb={6}>
|
|
<Box px={3} mb={8}>
|
|
<Flex align="center" gap={3} mb={2}>
|
|
<Image
|
|
src={publicSettings?.club_logo_url || '/dist/img/logo-club-empty.svg'}
|
|
alt="Club Logo"
|
|
boxSize="48px"
|
|
objectFit="contain"
|
|
fallbackSrc="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23e2e8f0'/%3E%3Ctext x='50' y='55' text-anchor='middle' font-size='40' fill='%23718096'%3EMC%3C/text%3E%3C/svg%3E"
|
|
borderRadius="md"
|
|
/>
|
|
<VStack align="start" spacing={0}>
|
|
<Text
|
|
fontSize="xl"
|
|
fontWeight="extrabold"
|
|
color={useColorModeValue('gray.800', 'white')}
|
|
letterSpacing="tight"
|
|
>
|
|
My Club
|
|
</Text>
|
|
<Text fontSize="xs" color={useColorModeValue('gray.500', 'gray.400')} fontWeight="semibold" textTransform="uppercase" letterSpacing="wider">
|
|
Admin Panel
|
|
</Text>
|
|
</VStack>
|
|
</Flex>
|
|
</Box>
|
|
|
|
<NavItem
|
|
icon={FaHome}
|
|
to="/"
|
|
onClick={onClose}
|
|
>
|
|
Zpět na web
|
|
</NavItem>
|
|
|
|
<Divider my={2} />
|
|
|
|
{/* Dynamic Navigation */}
|
|
{navLoading ? (
|
|
<Flex justify="center" py={8}>
|
|
<Spinner size="sm" />
|
|
</Flex>
|
|
) : 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 (
|
|
<NavItem
|
|
key={item.id || index}
|
|
icon={itemIcon}
|
|
to={itemUrl}
|
|
onClick={onClose}
|
|
>
|
|
<Text as="span">
|
|
{item.label}
|
|
{showBadge && (
|
|
<Text as="span" ml={2} fontSize="10px" px={2} py={0.5} borderRadius="full" bg={useColorModeValue('green.100','green.900')} color={useColorModeValue('green.700','green.200')} borderWidth="1px" borderColor={useColorModeValue('green.200','green.700')}>
|
|
{upcomingCount}
|
|
</Text>
|
|
)}
|
|
</Text>
|
|
</NavItem>
|
|
);
|
|
})}
|
|
|
|
{/* MyUIbrix Editor - Special item */}
|
|
<NavItem
|
|
icon={FaPaintBrush}
|
|
onClick={(e) => {
|
|
e?.preventDefault();
|
|
window.open('/?myuibrix=edit', '_blank');
|
|
}}
|
|
>
|
|
MyUIbrix Editor
|
|
</NavItem>
|
|
</>
|
|
) : (
|
|
// Fallback to hardcoded navigation
|
|
<>
|
|
<Text fontSize="xs" fontWeight="bold" px={4} py={2} color={useColorModeValue('gray.500', 'gray.400')} textTransform="uppercase" letterSpacing="wider">
|
|
Hlavní
|
|
</Text>
|
|
|
|
<NavItem
|
|
icon={FaTachometerAlt}
|
|
to="/admin"
|
|
onClick={onClose}
|
|
>
|
|
Nástěnka
|
|
</NavItem>
|
|
|
|
{isAdmin && (
|
|
<NavItem
|
|
icon={FaChartBar}
|
|
to="/admin/analytika"
|
|
onClick={onClose}
|
|
>
|
|
Analytika
|
|
</NavItem>
|
|
)}
|
|
|
|
<Text fontSize="xs" fontWeight="bold" px={4} py={2} color={useColorModeValue('gray.500', 'gray.400')} textTransform="uppercase" letterSpacing="wider" mt={4}>
|
|
Obsah
|
|
</Text>
|
|
{/* Core sports entities first */}
|
|
<NavItem
|
|
icon={FaUsers}
|
|
to="/admin/tymy"
|
|
onClick={onClose}
|
|
>
|
|
Týmy
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaCalendarAlt}
|
|
to="/admin/zapasy"
|
|
onClick={onClose}
|
|
>
|
|
{/* Add subtle scroller hint */}
|
|
<Text as="span">
|
|
Zápasy
|
|
<Text as="span" ml={2} fontSize="10px" px={2} py={0.5} borderRadius="full" bg={useColorModeValue('gray.100','whiteAlpha.200')} color={useColorModeValue('gray.700','gray.300')} borderWidth="1px" borderColor={useColorModeValue('gray.200','whiteAlpha.300')}>
|
|
scroller
|
|
</Text>
|
|
</Text>
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaCalendarAlt}
|
|
to="/admin/aktivity"
|
|
onClick={onClose}
|
|
>
|
|
<Text as="span">
|
|
Aktivity
|
|
{upcomingCount > 0 && (
|
|
<Text as="span" ml={2} fontSize="10px" px={2} py={0.5} borderRadius="full" bg={useColorModeValue('green.100','green.900')} color={useColorModeValue('green.700','green.200')} borderWidth="1px" borderColor={useColorModeValue('green.200','green.700')}>
|
|
{upcomingCount}
|
|
</Text>
|
|
)}
|
|
</Text>
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaFutbol}
|
|
to="/admin/hraci"
|
|
onClick={onClose}
|
|
>
|
|
Hráči
|
|
</NavItem>
|
|
{/* Other content */}
|
|
<NavItem
|
|
icon={FaNewspaper}
|
|
to="/admin/clanky"
|
|
onClick={onClose}
|
|
>
|
|
Články
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaFileAlt}
|
|
to="/admin/kategorie"
|
|
onClick={onClose}
|
|
>
|
|
Kategorie
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaBook}
|
|
to="/admin/o-klubu"
|
|
onClick={onClose}
|
|
>
|
|
O klubu
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaImage}
|
|
to="/admin/videa"
|
|
onClick={onClose}
|
|
>
|
|
Videa
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaImage}
|
|
to="/admin/galerie"
|
|
onClick={onClose}
|
|
>
|
|
Galerie (Zonerama)
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaTachometerAlt}
|
|
to="/admin/scoreboard"
|
|
onClick={onClose}
|
|
>
|
|
Tabule (Scoreboard)
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaMobileAlt}
|
|
to="/admin/scoreboard/remote"
|
|
onClick={onClose}
|
|
>
|
|
Scoreboard Remote
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaPalette}
|
|
to="/admin/obleceni"
|
|
onClick={onClose}
|
|
>
|
|
Oblečení
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaHandshake}
|
|
to="/admin/sponzori"
|
|
onClick={onClose}
|
|
>
|
|
Sponzoři
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaImage}
|
|
to="/admin/bannery"
|
|
onClick={onClose}
|
|
>
|
|
Bannery
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaEnvelope}
|
|
to="/admin/zpravy"
|
|
onClick={onClose}
|
|
>
|
|
Zprávy
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaAddressBook}
|
|
to="/admin/kontakty"
|
|
onClick={onClose}
|
|
>
|
|
Kontakty
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaPaperPlane}
|
|
to="/admin/newsletter"
|
|
onClick={onClose}
|
|
>
|
|
Zpravodaj
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaPoll}
|
|
to="/admin/ankety"
|
|
onClick={onClose}
|
|
>
|
|
Ankety
|
|
</NavItem>
|
|
|
|
<Divider my={2} />
|
|
|
|
{isAdmin && (
|
|
<>
|
|
<Text fontSize="xs" fontWeight="bold" px={4} py={2} color={useColorModeValue('gray.500', 'gray.400')} textTransform="uppercase" letterSpacing="wider" mt={4}>
|
|
Nastavení
|
|
</Text>
|
|
|
|
<NavItem
|
|
icon={FaPaintBrush}
|
|
onClick={(e) => {
|
|
e?.preventDefault();
|
|
window.open('/?myuibrix=edit', '_blank');
|
|
}}
|
|
>
|
|
MyUIbrix Editor
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
icon={FaBars}
|
|
to="/admin/navigace"
|
|
onClick={onClose}
|
|
>
|
|
Navigace
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
icon={FaAward}
|
|
to="/admin/aliasy-soutezi"
|
|
onClick={onClose}
|
|
>
|
|
Alias soutěží
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
icon={FaSyncAlt}
|
|
to="/admin/prefetch"
|
|
onClick={onClose}
|
|
>
|
|
Prefetch & Cache
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
icon={FaUsers}
|
|
to="/admin/uzivatele"
|
|
onClick={onClose}
|
|
>
|
|
Uživatelé
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaPalette}
|
|
to="/admin/nastaveni"
|
|
onClick={onClose}
|
|
>
|
|
Nastavení
|
|
</NavItem>
|
|
<NavItem
|
|
icon={FaFolder}
|
|
to="/admin/soubory"
|
|
onClick={onClose}
|
|
>
|
|
Soubory
|
|
</NavItem>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<Box mt="auto" mb={4} px={2}>
|
|
<ChakraLink
|
|
as="button"
|
|
display="flex"
|
|
alignItems="center"
|
|
w="100%"
|
|
px={4}
|
|
py={2}
|
|
borderRadius="md"
|
|
_hover={{
|
|
textDecoration: 'none',
|
|
bg: useColorModeValue('red.50', 'red.900'),
|
|
color: 'red.500',
|
|
}}
|
|
onClick={logout}
|
|
color={useColorModeValue('red.500', 'red.300')}
|
|
>
|
|
<Icon as={FaSignOutAlt} mr={3} />
|
|
<Text>Odhlásit se</Text>
|
|
</ChakraLink>
|
|
</Box>
|
|
</VStack>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default AdminSidebar;
|