Files
MyClub/frontend/src/components/elements/SpartaNavbar.tsx
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

222 lines
8.4 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { Link as RouterLink, useLocation } from 'react-router-dom';
import '../../styles/sparta-styles.css';
import { usePublicSettings } from '../../hooks/usePublicSettings';
import { useClubTheme } from '../../contexts/ClubThemeContext';
import { getNavigationItems, NavigationItem, seedDefaultNavigation } from '../../services/navigation';
import { getCategories, Category } from '../../services/public';
import { assetUrl } from '../../utils/url';
import { useTranslation } from 'react-i18next';
// Minimal NavLink type used to render items
type NavLink = { label: string; to?: string; items?: { label: string; to: string }[]; external?: boolean };
const SpartaNavbar: React.FC = () => {
const { t } = useTranslation();
const { data: settings } = usePublicSettings();
const theme = useClubTheme();
const location = useLocation();
const [mobileOpen, setMobileOpen] = useState(false);
const [dynamicNavItems, setDynamicNavItems] = useState<NavigationItem[]>([]);
const [navLoading, setNavLoading] = useState(true);
const [navCategories, setNavCategories] = useState<Category[] | null>(null);
// Load dynamic navigation
useEffect(() => {
let active = true;
(async () => {
try {
const items = await getNavigationItems();
if (active && Array.isArray(items)) {
const publicItems = items.filter(item => !item.requires_admin);
if (publicItems.length === 0) {
try {
await seedDefaultNavigation();
const newItems = await getNavigationItems();
if (active && Array.isArray(newItems)) {
const publicNewItems = newItems.filter(item => !item.requires_admin);
setDynamicNavItems(publicNewItems);
}
} catch {
setDynamicNavItems([]);
}
} else {
setDynamicNavItems(publicItems);
}
}
} catch {
// leave empty, fallback will handle
} finally {
if (active) setNavLoading(false);
}
})();
return () => { active = false };
}, []);
// Load categories (for Blog dropdown fallback)
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]);
const isPathActive = (to?: string) => {
if (!to) return false;
return location.pathname === to || location.pathname.startsWith((to || '') + '/');
};
const convertToNavLink = (item: NavigationItem): NavLink => {
// Map known Czech labels to translation keys
const getTranslatedLabel = (label: string) => {
const labelMap: Record<string, string> = {
'Domů': 'nav.home',
'Aktuality': 'nav.news',
'Zápasy': 'nav.matches',
'Hráči': 'nav.players',
'Fotogalerie': 'nav.gallery',
'Videa': 'nav.videos',
'Kontakt': 'nav.contact',
'O klubu': 'nav.about',
'Aktivity': 'nav.activities',
'Sponzoři': 'nav.sponsors',
'Články': 'nav.news',
'Blog': 'nav.news',
'Kalendář': 'nav.calendar',
'Tabulky': 'nav.table'
};
const translationKey = labelMap[label];
return translationKey ? t(translationKey) : label;
};
const link: NavLink = {
label: getTranslatedLabel(item.label),
to: item.url || '#',
external: item.type === 'external',
};
if (item.type === 'dropdown' && item.children && item.children.length > 0) {
link.items = item.children.map(child => ({
label: getTranslatedLabel(child.label),
to: child.url || '#'
}));
}
return link;
};
const categoryItems = useMemo(() => {
const source = Array.isArray(navCategories) && navCategories.length > 0 ? navCategories : [];
return source.map((cat: any) => ({ label: cat.name, to: cat.url || (cat.id ? `/blog?category_id=${cat.id}` : (cat.slug ? `/blog?category=${encodeURIComponent(cat.slug)}` : '/blog')) }));
}, [navCategories]);
const NAV_LINKS: NavLink[] = useMemo(() => {
if (!navLoading && dynamicNavItems.length > 0) {
const navLinks = dynamicNavItems.map(convertToNavLink);
if (categoryItems.length > 0) {
const idx = navLinks.findIndex(l => l.label === 'Články' || l.label === 'Blog' || l.to === '/blog');
if (idx !== -1) navLinks[idx] = { ...navLinks[idx], items: categoryItems };
}
return navLinks;
}
// Fallback minimal menu
const 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' },
categoryItems.length > 0 ? { label: 'Články', to: '/blog', items: categoryItems } : { label: 'Články', to: '/blog' },
{ label: 'Videa', to: '/videa' },
{ label: settings?.gallery_label || t('nav.gallery'), to: '/galerie' },
...(settings?.shop_url ? [{ label: 'Fanshop', to: settings.shop_url, external: true } as NavLink] : []),
{ label: 'Sponzoři', to: '/sponzori' },
{ label: 'Kontakt', to: '/kontakt' },
];
return links;
}, [navLoading, dynamicNavItems, settings?.show_about_in_nav, settings?.shop_url, settings?.gallery_label, categoryItems]);
const logoUrl = (assetUrl(settings?.club_logo_url || theme.logoUrl) || settings?.club_logo_url || theme.logoUrl) || '/dist/img/logo-club-empty.svg';
const clubName = settings?.club_name || theme.name || 'Klub';
return (
<div className="sparta-navbar-container">
<div className="sparta-navbar">
{/* Burger toggle for mobile */}
<button
aria-label="Menu"
className="sparta-navbar-toggle"
onClick={() => setMobileOpen(o => !o)}
>
<div className="sparta-burger-icon" aria-hidden>
<div className="sparta-burger-line" />
<div className="sparta-burger-line" />
<div className="sparta-burger-line" />
</div>
</button>
{/* Brand */}
<RouterLink to="/" className="sparta-navbar-brand" onClick={() => setMobileOpen(false)}>
<img src={logoUrl} alt={clubName} />
</RouterLink>
{/* Links */}
<nav
className="sparta-navbar-links"
style={{ display: mobileOpen ? 'flex' : undefined, flexWrap: 'wrap' }}
>
{NAV_LINKS.map((nav) => {
const isActive = isPathActive(nav.to);
const className = isActive ? 'sparta-button-tertiary' : 'sparta-button-tertiary';
// When categories are present under Články/Blog, render clickable parent + category links
if (nav.items && nav.items.length > 0 && (nav.label === 'Články' || nav.label === 'Blog' || (nav.to || '').startsWith('/blog'))) {
return (
<React.Fragment key={nav.label}>
<RouterLink to={nav.to || '/blog'} className={className} onClick={() => setMobileOpen(false)}>
{nav.label}
</RouterLink>
{nav.items.map((it) => (
<RouterLink key={`${nav.label}-${it.to}`} to={it.to} className={className} onClick={() => setMobileOpen(false)}>
{it.label}
</RouterLink>
))}
</React.Fragment>
);
}
if (nav.external && nav.to) {
return (
<a key={nav.label} href={nav.to} target="_blank" rel="noreferrer" className={className} onClick={() => setMobileOpen(false)}>
{nav.label}
</a>
);
}
return (
<RouterLink key={nav.label} to={nav.to || '#'} className={className} onClick={() => setMobileOpen(false)}>
{nav.label}
</RouterLink>
);
})}
</nav>
</div>
</div>
);
};
export default SpartaNavbar;