mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
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';
|
||||
|
||||
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 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="/api/logo"
|
||||
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;
|
||||
Reference in New Issue
Block a user