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,717 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
useColorModeValue,
|
||||
useColorMode,
|
||||
IconButton,
|
||||
Avatar,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Text,
|
||||
HStack,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
DrawerContent,
|
||||
DrawerCloseButton,
|
||||
VStack,
|
||||
Divider,
|
||||
Container,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Input,
|
||||
} from '@chakra-ui/react';
|
||||
import { MoonIcon, SunIcon, HamburgerIcon, EditIcon, ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import { FaFacebook, FaInstagram, FaYoutube, FaPhotoVideo, FaExternalLinkAlt, FaShoppingBag, FaCamera, FaSearch } from 'react-icons/fa';
|
||||
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { usePublicSettings } from '../hooks/usePublicSettings';
|
||||
import { useClubTheme } from '../contexts/ClubThemeContext';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { getCategories, Category } from '../services/public';
|
||||
import { FaSearch as FaSearchIcon } from 'react-icons/fa';
|
||||
import { getNavigationItems, NavigationItem, seedDefaultNavigation } from '../services/navigation';
|
||||
|
||||
type NavLink = { label: string; to?: string; items?: { label: string; to: string }[]; external?: boolean };
|
||||
|
||||
// Minimal normalization for social URLs so admins can input @handle or domain-less usernames
|
||||
const normalizeSocialUrl = (network: 'facebook' | 'instagram' | 'youtube', raw?: string | null): string | null => {
|
||||
let v = String(raw || '').trim();
|
||||
if (!v) return null;
|
||||
v = v.replace(/\s+/g, '');
|
||||
if (v.startsWith('@')) {
|
||||
const handle = v.slice(1);
|
||||
if (network === 'facebook') return `https://www.facebook.com/${handle}`;
|
||||
if (network === 'instagram') return `https://www.instagram.com/${handle}`;
|
||||
if (network === 'youtube') return `https://www.youtube.com/@${handle}`;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(v) && !v.includes('/') && !v.includes('.')) {
|
||||
if (network === 'facebook') return `https://www.facebook.com/${v}`;
|
||||
if (network === 'instagram') return `https://www.instagram.com/${v}`;
|
||||
if (network === 'youtube') return `https://www.youtube.com/@${v}`;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(v)) {
|
||||
if (/^facebook\.com\//i.test(v)) return `https://www.${v}`;
|
||||
if (/^instagram\.com\//i.test(v)) return `https://www.${v}`;
|
||||
if (/^youtube\.com\//i.test(v)) return `https://www.${v}`;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
// Mobile menu component
|
||||
const MobileMenu = ({ isOpen, onClose, isAdmin, menuBg, dividerColor, settings, categories, galleryHref, galleryLabel, hasTables, dynamicNavItems, navLoading }: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isAdmin: boolean;
|
||||
menuBg: string;
|
||||
dividerColor: string;
|
||||
settings?: any;
|
||||
categories?: Category[] | null;
|
||||
galleryHref?: string | null;
|
||||
galleryLabel?: string;
|
||||
hasTables?: boolean | null;
|
||||
dynamicNavItems: NavigationItem[];
|
||||
navLoading: boolean;
|
||||
}) => (
|
||||
<Drawer isOpen={isOpen} placement="left" onClose={onClose}>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent bg={menuBg}>
|
||||
<DrawerCloseButton />
|
||||
<DrawerHeader borderBottomWidth="1px" borderColor="border.subtle">Menu</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{/* Dynamic navigation items in mobile */}
|
||||
{(!navLoading && dynamicNavItems.length > 0) ? (
|
||||
// Use dynamic navigation
|
||||
dynamicNavItems.map((item, idx) => {
|
||||
const linkIsExternal = item.type === 'external';
|
||||
const hasChildren = item.type === 'dropdown' && item.children && item.children.length > 0;
|
||||
const linkProps = linkIsExternal ? { href: item.url } : { to: item.url || '/' };
|
||||
const Comp: any = linkIsExternal ? 'a' : RouterLink;
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id || idx}>
|
||||
<Button
|
||||
as={Comp}
|
||||
{...linkProps}
|
||||
target={linkIsExternal ? '_blank' : undefined}
|
||||
rel={linkIsExternal ? 'noreferrer' : undefined}
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
fontWeight={hasChildren ? 'bold' : 'normal'}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
{/* Render children for dropdown items */}
|
||||
{hasChildren && (
|
||||
<VStack align="stretch" pl={4} spacing={1}>
|
||||
{item.children!.map((child) => {
|
||||
const childIsExternal = child.type === 'external';
|
||||
const childLinkProps = childIsExternal ? { href: child.url } : { to: child.url || '/' };
|
||||
const ChildComp: any = childIsExternal ? 'a' : RouterLink;
|
||||
return (
|
||||
<Button
|
||||
key={child.id}
|
||||
as={ChildComp}
|
||||
{...(childLinkProps as any)}
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
fontWeight="normal"
|
||||
size="sm"
|
||||
>
|
||||
{child.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
// Fallback to hardcoded navigation
|
||||
<>
|
||||
<Button as={RouterLink} to="/" variant="ghost" justifyContent="flex-start">Domů</Button>
|
||||
{(settings?.show_about_in_nav ?? true) && (
|
||||
<Button as={RouterLink} to="/o-klubu" variant="ghost" justifyContent="flex-start">O klubu</Button>
|
||||
)}
|
||||
<Button as={RouterLink} to="/kalendar" variant="ghost" justifyContent="flex-start">Kalendář</Button>
|
||||
<Button as={RouterLink} to="/zapasy" variant="ghost" justifyContent="flex-start">Zápasy</Button>
|
||||
<Button as={RouterLink} to="/aktivity" variant="ghost" justifyContent="flex-start">Aktivity</Button>
|
||||
<Button as={RouterLink} to="/hraci" variant="ghost" justifyContent="flex-start">Hráči</Button>
|
||||
{hasTables ? (
|
||||
<Button as={RouterLink} to="/tabulky" variant="ghost" justifyContent="flex-start">Tabulky</Button>
|
||||
) : null}
|
||||
{Array.isArray(settings?.custom_nav) && settings.custom_nav.length > 0 && settings.custom_nav.map((item: any, idx: number) => {
|
||||
const customLinkIsExternal = typeof item?.url === 'string' && /^https?:\/\//i.test(item.url);
|
||||
const linkProps = customLinkIsExternal ? { href: item.url } : { to: item.url || '/' };
|
||||
const Comp: any = customLinkIsExternal ? 'a' : RouterLink;
|
||||
return (
|
||||
<Button
|
||||
key={`custom-nav-${idx}-${item?.label || 'link'}`}
|
||||
as={Comp}
|
||||
{...linkProps}
|
||||
target={customLinkIsExternal ? '_blank' : undefined}
|
||||
rel={customLinkIsExternal ? 'noreferrer' : undefined}
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
{item?.label || 'Stránka'}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
<Button as={RouterLink} to="/blog" variant="ghost" justifyContent="flex-start" fontWeight="bold">Články</Button>
|
||||
{Array.isArray(categories) && categories.length > 0 && (
|
||||
<VStack align="stretch" pl={4} spacing={1}>
|
||||
{categories.map((cat: any) => {
|
||||
const catIsExternal = typeof cat.url === 'string' && /^https?:\/\//i.test(cat.url);
|
||||
const catHref = cat.url || (cat.slug ? `/blog?category=${encodeURIComponent(cat.slug)}` : '/blog');
|
||||
const catLinkProps = catIsExternal ? { href: catHref } : { to: catHref };
|
||||
return (
|
||||
<Button key={cat.slug || cat.name} as={catIsExternal ? 'a' : RouterLink} {...(catLinkProps as any)} variant="ghost" justifyContent="flex-start" fontWeight="normal" size="sm">
|
||||
{cat.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
<Button as={RouterLink} to="/videa" variant="ghost" justifyContent="flex-start">Videa</Button>
|
||||
<Button as={RouterLink} to="/hledat" variant="ghost" justifyContent="flex-start">Hledat</Button>
|
||||
<Button as={RouterLink} to="/galerie" variant="ghost" justifyContent="flex-start">{galleryLabel || 'Fotogalerie'}</Button>
|
||||
{settings?.shop_url && (
|
||||
<Button as="a" href={settings.shop_url} target="_blank" rel="noreferrer" variant="ghost" justifyContent="flex-start">Fanshop</Button>
|
||||
)}
|
||||
<Button as={RouterLink} to="/sponzori" variant="ghost" justifyContent="flex-start">Sponzoři</Button>
|
||||
<Button as={RouterLink} to="/kontakt" variant="ghost" justifyContent="flex-start">Kontakt</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Divider my={2} borderColor={dividerColor} />
|
||||
<Text fontWeight="bold" mt={2} color={dividerColor}>Administrace</Text>
|
||||
<Button as={RouterLink} to="/admin" variant="ghost" justifyContent="flex-start" colorScheme="blue">
|
||||
Administrace
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
const Navbar = () => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { isAuthenticated, logout, user } = useAuth();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: isSearchOpen, onOpen: onSearchOpen, onClose: onSearchClose } = useDisclosure();
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const { data: settings } = usePublicSettings();
|
||||
const theme = useClubTheme();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const menuBg = useColorModeValue('white', '#0f1115');
|
||||
const dividerColor = useColorModeValue('gray.600', 'gray.300');
|
||||
const hoverBg = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
|
||||
const activeBg = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
|
||||
const activeTextColor = useColorModeValue('brand.primary', 'brand.accent');
|
||||
const navTextColor = useColorModeValue('gray.700', 'gray.200');
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [hasTables, setHasTables] = useState<boolean | null>(null);
|
||||
const [dynamicNavItems, setDynamicNavItems] = useState<NavigationItem[]>([]);
|
||||
const [navLoading, setNavLoading] = useState(true);
|
||||
|
||||
// Search modal state
|
||||
const [query, setQuery] = useState('');
|
||||
const submitSearch = () => {
|
||||
const text = query.trim();
|
||||
if (!text) return;
|
||||
onSearchClose();
|
||||
setQuery('');
|
||||
navigate(`/hledat?q=${encodeURIComponent(text)}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 8);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true } as any);
|
||||
return () => window.removeEventListener('scroll', onScroll as any);
|
||||
}, []);
|
||||
|
||||
// Also set document title to club name ASAP (SEO component will refine further)
|
||||
useEffect(() => {
|
||||
const name = settings?.club_name || theme.name;
|
||||
if (name && typeof document !== 'undefined') {
|
||||
document.title = name;
|
||||
}
|
||||
}, [settings?.club_name, theme.name]);
|
||||
|
||||
// Set favicon/logo in head for fan pages (SPA)
|
||||
useEffect(() => {
|
||||
try {
|
||||
let url = settings?.club_logo_url || theme.logoUrl || '/dist/img/logo-club-empty.svg';
|
||||
if (!url) return;
|
||||
// Normalize relative upload paths to API origin so favicon resolves on all pages
|
||||
try {
|
||||
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1';
|
||||
const apiOrigin = new URL(apiUrl).origin;
|
||||
if (/^\/.+/.test(url) && !/^https?:\/\//i.test(url)) {
|
||||
// If starts with /uploads or any absolute path, prefix API origin
|
||||
url = apiOrigin + url;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const setIcon = (rel: string) => {
|
||||
let link = document.querySelector<HTMLLinkElement>(`link[rel="${rel}"]`);
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = rel as any;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.href = url;
|
||||
// Try to hint type if svg
|
||||
if (url.endsWith('.svg')) link.type = 'image/svg+xml';
|
||||
};
|
||||
setIcon('icon');
|
||||
setIcon('shortcut icon');
|
||||
} catch {}
|
||||
}, [settings?.club_logo_url, theme.logoUrl]);
|
||||
|
||||
// gallery link (generic first, fallback to zonerama)
|
||||
const galleryHref = settings?.gallery_url || settings?.zonerama_url;
|
||||
const galleryLabel = settings?.gallery_label || 'Fotogalerie';
|
||||
|
||||
// Load dynamic navigation from API
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
const items = await getNavigationItems();
|
||||
if (active && Array.isArray(items)) {
|
||||
// Filter out admin-only navigation items for public display
|
||||
const publicItems = items.filter(item => !item.requires_admin);
|
||||
|
||||
// Auto-seed if navigation is empty (only if user is authenticated as admin)
|
||||
if (publicItems.length === 0 && isAdmin) {
|
||||
try {
|
||||
console.log('Navigation empty, auto-seeding...');
|
||||
await seedDefaultNavigation();
|
||||
const newItems = await getNavigationItems();
|
||||
if (active && Array.isArray(newItems)) {
|
||||
const publicNewItems = newItems.filter(item => !item.requires_admin);
|
||||
setDynamicNavItems(publicNewItems);
|
||||
}
|
||||
} catch (seedError) {
|
||||
console.error('Auto-seed failed:', seedError);
|
||||
// Continue with empty navigation
|
||||
}
|
||||
} else {
|
||||
setDynamicNavItems(publicItems);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load navigation:', error);
|
||||
} finally {
|
||||
if (active) setNavLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { active = false };
|
||||
}, [isAdmin]);
|
||||
|
||||
// categories: prefer API, fallback to settings.categories
|
||||
const [navCategories, setNavCategories] = useState<Category[] | null>(null);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
const cats = await getCategories();
|
||||
if (active && Array.isArray(cats) && cats.length > 0) {
|
||||
setNavCategories(cats);
|
||||
} else if (active && Array.isArray(settings?.categories)) {
|
||||
setNavCategories(settings!.categories as any);
|
||||
}
|
||||
} catch {
|
||||
if (active && Array.isArray(settings?.categories)) {
|
||||
setNavCategories(settings!.categories as any);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => { active = false };
|
||||
}, [settings?.categories]);
|
||||
|
||||
// Determine if there is any table data available (prefetch snapshot)
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
const resolveBackendUrl = (path: string) => {
|
||||
try {
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) {
|
||||
const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1';
|
||||
const origin = new URL(base).origin;
|
||||
return new URL(path, origin).toString();
|
||||
}
|
||||
return path;
|
||||
} catch { return path; }
|
||||
};
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(resolveBackendUrl('/cache/prefetch/facr_tables.json'), { cache: 'no-cache' });
|
||||
if (!res.ok) { if (!disposed) setHasTables(false); return; }
|
||||
const json = await res.json();
|
||||
const anyRows = Array.isArray(json?.competitions) && json.competitions.some((c: any) => Array.isArray(c?.table?.overall) && c.table.overall.length > 0);
|
||||
if (!disposed) setHasTables(!!anyRows);
|
||||
} catch {
|
||||
if (!disposed) setHasTables(false);
|
||||
}
|
||||
})();
|
||||
return () => { disposed = true; };
|
||||
}, []);
|
||||
|
||||
const isPathActive = (to?: string) => {
|
||||
if (!to) return false;
|
||||
// Active when current pathname starts with target (handles subroutes)
|
||||
return location.pathname === to || location.pathname.startsWith(to + '/');
|
||||
};
|
||||
|
||||
// Convert NavigationItem to NavLink format
|
||||
const convertToNavLink = (item: NavigationItem): NavLink => {
|
||||
const link: NavLink = {
|
||||
label: item.label,
|
||||
to: item.url || '#',
|
||||
external: item.type === 'external',
|
||||
};
|
||||
|
||||
// Add children for dropdown items
|
||||
if (item.type === 'dropdown' && item.children && item.children.length > 0) {
|
||||
link.items = item.children.map(child => ({
|
||||
label: child.label,
|
||||
to: child.url || '#',
|
||||
}));
|
||||
}
|
||||
|
||||
return link;
|
||||
};
|
||||
|
||||
// Build categories as items for Články dropdown (fallback)
|
||||
const categoryItems = useMemo(() => {
|
||||
const source = Array.isArray(navCategories) && navCategories.length > 0 ? navCategories : [];
|
||||
return source.map((cat: any) => ({
|
||||
label: cat.name,
|
||||
to: cat.url || (cat.slug ? `/blog?category=${encodeURIComponent(cat.slug)}` : '/blog')
|
||||
}));
|
||||
}, [navCategories]);
|
||||
|
||||
// Use dynamic navigation if available, otherwise fallback to hardcoded
|
||||
let NAV_LINKS: NavLink[] = useMemo(() => {
|
||||
if (!navLoading && dynamicNavItems.length > 0) {
|
||||
// Use dynamic navigation from API
|
||||
return dynamicNavItems.map(convertToNavLink);
|
||||
}
|
||||
|
||||
// Fallback to hardcoded navigation
|
||||
let links: NavLink[] = [
|
||||
{ label: 'Domů', to: '/' },
|
||||
...(settings?.show_about_in_nav === false ? [] : [{ label: 'O klubu', to: '/o-klubu' } as NavLink]),
|
||||
{ label: 'Kalendář', to: '/kalendar' },
|
||||
{ label: 'Zápasy', to: '/zapasy' },
|
||||
{ label: 'Aktivity', to: '/aktivity' },
|
||||
{ label: 'Hráči', to: '/hraci' },
|
||||
{ label: 'Tabulky', to: '/tabulky' },
|
||||
// Články with categories as subcategories
|
||||
categoryItems.length > 0
|
||||
? { label: 'Články', to: '/blog', items: categoryItems }
|
||||
: { label: 'Články', to: '/blog' },
|
||||
{ label: 'Videa', to: '/videa' },
|
||||
{ label: galleryLabel, to: '/galerie' },
|
||||
...(settings?.shop_url ? [{ label: 'Fanshop', to: settings.shop_url, external: true } as NavLink] : []),
|
||||
{ label: 'Sponzoři', to: '/sponzori' },
|
||||
{ label: 'Kontakt', to: '/kontakt' },
|
||||
];
|
||||
|
||||
// Inject custom pages from settings.custom_nav (label + url + external?)
|
||||
const customNav = Array.isArray((settings as any)?.custom_nav) ? ((settings as any).custom_nav as any[]) : [];
|
||||
if (customNav.length > 0) {
|
||||
const mapped: NavLink[] = customNav.map((it) => ({ label: String(it.label || 'Stránka'), to: String(it.url || '#'), external: Boolean(it.external) }));
|
||||
const insertIdx = links.findIndex((n) => n.label === 'Tabulky');
|
||||
if (insertIdx >= 0) {
|
||||
links = [...links.slice(0, insertIdx + 1), ...mapped, ...links.slice(insertIdx + 1)];
|
||||
} else {
|
||||
links = [...links, ...mapped];
|
||||
}
|
||||
}
|
||||
|
||||
// Hide Tabulky when there is no table data
|
||||
if (hasTables === false) {
|
||||
links = links.filter((n) => n.label !== 'Tabulky');
|
||||
}
|
||||
|
||||
return links;
|
||||
}, [dynamicNavItems, navLoading, settings, categoryItems, hasTables, galleryLabel]);
|
||||
|
||||
return (
|
||||
<Box position="sticky" top={0} zIndex={1000}>
|
||||
{/* Top bar with socials and quick external links */}
|
||||
{(settings?.facebook_url || settings?.instagram_url || settings?.youtube_url || settings?.shop_url) && (
|
||||
<Box bg={useColorModeValue('gray.50', 'blackAlpha.500')} borderBottomWidth="1px" borderColor="border.subtle" py={1}>
|
||||
<Container maxW="7xl">
|
||||
<Flex align="center" justify="space-between" gap={2}>
|
||||
<HStack spacing={2}>
|
||||
{settings?.shop_url && (
|
||||
<Button as="a" href={settings.shop_url} target="_blank" rel="noreferrer" variant="link" size="xs" leftIcon={<FaShoppingBag />}>
|
||||
Fanshop
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack spacing={1}>
|
||||
{normalizeSocialUrl('facebook', settings?.facebook_url) && (
|
||||
<IconButton as="a" href={normalizeSocialUrl('facebook', settings?.facebook_url) || undefined} target="_blank" rel="noreferrer" aria-label="Facebook" icon={<FaFacebook />} variant="ghost" size="xs" />
|
||||
)}
|
||||
{normalizeSocialUrl('instagram', settings?.instagram_url) && (
|
||||
<IconButton as="a" href={normalizeSocialUrl('instagram', settings?.instagram_url) || undefined} target="_blank" rel="noreferrer" aria-label="Instagram" icon={<FaInstagram />} variant="ghost" size="xs" />
|
||||
)}
|
||||
{normalizeSocialUrl('youtube', settings?.youtube_url) && (
|
||||
<IconButton as="a" href={normalizeSocialUrl('youtube', settings?.youtube_url) || undefined} target="_blank" rel="noreferrer" aria-label="YouTube" icon={<FaYoutube />} variant="ghost" size="xs" />
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Main Nav Bar */}
|
||||
<Box
|
||||
bg={useColorModeValue('rgba(255,255,255,0.9)', 'rgba(15,17,21,0.85)')}
|
||||
backdropFilter="saturate(180%) blur(10px)"
|
||||
borderBottomWidth="1px"
|
||||
borderColor="border.subtle"
|
||||
boxShadow={scrolled ? 'sm' : 'none'}
|
||||
transition="box-shadow 0.2s ease, background-color 0.2s ease, backdrop-filter 0.2s ease"
|
||||
>
|
||||
<MobileMenu isOpen={isOpen} onClose={onClose} isAdmin={isAdmin} menuBg={menuBg} dividerColor={dividerColor} settings={settings} categories={navCategories} galleryHref={galleryHref} galleryLabel={galleryLabel} hasTables={hasTables} dynamicNavItems={dynamicNavItems} navLoading={navLoading} />
|
||||
<Container maxW="7xl">
|
||||
<Flex h={16} alignItems="center" justifyContent="space-between">
|
||||
<HStack spacing={4} alignItems="center">
|
||||
{/* Club Logo only */}
|
||||
<HStack as={RouterLink} to="/" spacing={3} align="center">
|
||||
{(settings?.club_logo_url || theme.logoUrl) && (
|
||||
<Image
|
||||
src={settings?.club_logo_url || theme.logoUrl}
|
||||
alt={settings?.club_name || theme.name || 'Logo'}
|
||||
boxSize={{ base: '36px', md: '40px' }}
|
||||
objectFit="contain"
|
||||
borderRadius="full"
|
||||
borderWidth="2px"
|
||||
borderColor="brand.primary"
|
||||
style={{
|
||||
padding: (settings?.club_logo_url || theme.logoUrl)?.includes('logoapi.sportcreative.eu') ? '4px' : '0px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
{/* Desktop navigation with hover dropdowns */}
|
||||
<HStack as="nav" spacing={1} display={{ base: 'none', lg: 'flex' }} ml={4}>
|
||||
{NAV_LINKS.map((nav) => {
|
||||
const commonProps = {
|
||||
variant: 'ghost' as const,
|
||||
size: 'sm' as const,
|
||||
px: 3,
|
||||
_hover: { bg: hoverBg, transform: 'translateY(-1px)' },
|
||||
fontWeight: isPathActive(nav.to) ? '700' : '600',
|
||||
color: isPathActive(nav.to) ? activeTextColor : navTextColor,
|
||||
bg: isPathActive(nav.to) ? activeBg : 'transparent',
|
||||
transition: 'all 0.2s',
|
||||
};
|
||||
|
||||
// Handle items with dropdown (like Články with categories)
|
||||
if (nav.items && nav.items.length > 0) {
|
||||
return (
|
||||
<HoverMenu key={nav.label} label={nav.label} items={nav.items} isActive={isPathActive(nav.to)} />
|
||||
);
|
||||
}
|
||||
|
||||
if (nav.external && nav.to) {
|
||||
return (
|
||||
<Button key={nav.label} as="a" href={nav.to} target="_blank" rel="noreferrer" rightIcon={<FaExternalLinkAlt />} {...commonProps}>
|
||||
{nav.label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button key={nav.label} as={RouterLink} to={nav.to || '#'} {...commonProps}>
|
||||
{nav.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<Flex alignItems="center">
|
||||
{/* Mobile menu button */}
|
||||
<IconButton
|
||||
display={{ base: 'flex', md: 'none' }}
|
||||
onClick={onOpen}
|
||||
icon={<HamburgerIcon />}
|
||||
aria-label="Otevřít menu"
|
||||
variant="ghost"
|
||||
mr={2}
|
||||
/>
|
||||
|
||||
{/* Space reserved (socials moved to top bar) */}
|
||||
<Box display={{ base: 'none', md: 'flex' }} mr={2} />
|
||||
|
||||
{/* Search button */}
|
||||
<Tooltip label="Hledat" hasArrow>
|
||||
<IconButton
|
||||
aria-label="Hledat"
|
||||
icon={<FaSearch />}
|
||||
size="sm"
|
||||
mr={2}
|
||||
variant="ghost"
|
||||
onClick={onSearchOpen}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Admin edit button */}
|
||||
{isAdmin && (
|
||||
<Tooltip label="Správa obsahu" hasArrow>
|
||||
<IconButton
|
||||
as={RouterLink}
|
||||
to="/admin"
|
||||
aria-label="Správa obsahu"
|
||||
icon={<EditIcon />}
|
||||
size="sm"
|
||||
mr={2}
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Color mode toggle */}
|
||||
<IconButton
|
||||
size="md"
|
||||
fontSize="lg"
|
||||
aria-label="Přepnout barevné téma"
|
||||
variant="ghost"
|
||||
color="current"
|
||||
onClick={toggleColorMode}
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
/>
|
||||
|
||||
{isAuthenticated && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rounded="full"
|
||||
variant="link"
|
||||
cursor="pointer"
|
||||
minW={0}
|
||||
ml={2}
|
||||
>
|
||||
<Avatar size="sm" name={user?.name || 'Uživatel'} />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem as={RouterLink} to="/admin/nastaveni">Můj účet</MenuItem>
|
||||
{isAdmin && <MenuItem as={RouterLink} to="/admin">Administrace</MenuItem>}
|
||||
<MenuItem onClick={logout}>Odhlásit se</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Flex>
|
||||
{/* Close outer Flex */}
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
{/* Search Modal */}
|
||||
<Modal isOpen={isSearchOpen} onClose={onSearchClose} size="md" motionPreset="scale">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Vyhledávání</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitSearch();
|
||||
}}
|
||||
>
|
||||
<VStack spacing={4}>
|
||||
<InputGroup size="lg">
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<FaSearchIcon />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="Hledat kluby, zápasy, články, hráče..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</InputGroup>
|
||||
<Button type="submit" colorScheme="blue" size="lg" w="full" leftIcon={<FaSearchIcon />}>
|
||||
Vyhledat
|
||||
</Button>
|
||||
</VStack>
|
||||
</form>
|
||||
<Text fontSize="sm" color="gray.500" mt={4} textAlign="center">
|
||||
Zadejte klíčová slova pro vyhledávání
|
||||
</Text>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// HoverMenu component for desktop dropdown nav
|
||||
const HoverMenu = ({ label, items, isActive }: { label: string; items: { label: string; to: string }[]; isActive?: boolean }) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
return (
|
||||
<Box onMouseEnter={onOpen} onMouseLeave={onClose}>
|
||||
<Menu isOpen={isOpen} placement="bottom-start" gutter={4}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
px={3}
|
||||
fontWeight={isActive ? '700' : '600'}
|
||||
color={useColorModeValue(isActive ? 'brand.primary' : 'gray.700', isActive ? 'brand.accent' : 'gray.200')}
|
||||
bg={isActive ? useColorModeValue('blackAlpha.50', 'whiteAlpha.100') : 'transparent'}
|
||||
_hover={{ bg: useColorModeValue('blackAlpha.100', 'whiteAlpha.200'), transform: 'translateY(-1px)' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
{label}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{items.map((it) => (
|
||||
<MenuItem as={RouterLink} to={it.to} key={it.to}>
|
||||
{it.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
||||
// Search Modal rendered alongside Navbar content
|
||||
// Note: We append the modal inside Navbar return to keep code compact
|
||||
Reference in New Issue
Block a user