This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
+717
View File
@@ -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