mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #75
This commit is contained in:
@@ -116,7 +116,37 @@ const MobileMenu = ({ isOpen, onClose, isAdmin, isAuthenticated, menuBg, divider
|
||||
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;
|
||||
|
||||
// Special handling for Blog/Články: when categories exist, make parent non-clickable and list categories
|
||||
const isArticlesLink = (item.label === 'Články' || item.label === 'Blog' || ((item.url || '') as string).startsWith('/blog'));
|
||||
const hasCats = Array.isArray(categories) && categories.length > 0;
|
||||
|
||||
if (isArticlesLink && hasCats) {
|
||||
return (
|
||||
<React.Fragment key={item.id || idx}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
<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.id ? `/blog?category_id=${cat.id}` : (cat.slug ? `/blog?category=${encodeURIComponent(cat.slug)}` : '/blog'));
|
||||
const catLinkProps = catIsExternal ? { href: catHref } : { to: catHref };
|
||||
const CatComp: any = catIsExternal ? 'a' : RouterLink;
|
||||
return (
|
||||
<Button key={cat.slug || cat.id || cat.name} as={CatComp} {...(catLinkProps as any)} variant="ghost" justifyContent="flex-start" fontWeight="normal" size="sm">
|
||||
{cat.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id || idx}>
|
||||
<Button
|
||||
@@ -194,20 +224,24 @@ const MobileMenu = ({ isOpen, onClose, isAdmin, isAuthenticated, menuBg, divider
|
||||
})}
|
||||
{hasArticles === true && (
|
||||
<>
|
||||
<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>
|
||||
{Array.isArray(categories) && categories.length > 0 ? (
|
||||
<>
|
||||
<Button variant="ghost" justifyContent="flex-start" fontWeight="bold">Články</Button>
|
||||
<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.id ? `/blog?category_id=${cat.id}` : (cat.slug ? `/blog?category=${encodeURIComponent(cat.slug)}` : '/blog'));
|
||||
const catLinkProps = catIsExternal ? { href: catHref } : { to: catHref };
|
||||
return (
|
||||
<Button key={cat.slug || cat.id || 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="/blog" variant="ghost" justifyContent="flex-start" fontWeight="bold">Články</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -282,6 +316,13 @@ const Navbar: React.FC<{ fullWidth?: boolean }> = ({ fullWidth = false }) => {
|
||||
const [dynamicNavItems, setDynamicNavItems] = useState<NavigationItem[]>([]);
|
||||
const [navLoading, setNavLoading] = useState(true);
|
||||
const containerMaxW = fullWidth ? 'full' as const : '7xl' as const;
|
||||
const [windowWidth, setWindowWidth] = useState<number>(typeof window !== 'undefined' ? window.innerWidth : 1920);
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => setWindowWidth(window.innerWidth);
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, []);
|
||||
|
||||
// Search modal state
|
||||
const [query, setQuery] = useState('');
|
||||
@@ -535,7 +576,7 @@ const Navbar: React.FC<{ fullWidth?: boolean }> = ({ fullWidth = false }) => {
|
||||
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')
|
||||
to: cat.url || (cat.id ? `/blog?category_id=${cat.id}` : (cat.slug ? `/blog?category=${encodeURIComponent(cat.slug)}` : '/blog'))
|
||||
}));
|
||||
}, [navCategories]);
|
||||
|
||||
@@ -659,6 +700,32 @@ const Navbar: React.FC<{ fullWidth?: boolean }> = ({ fullWidth = false }) => {
|
||||
return links;
|
||||
}, [filteredDynamicNavItems, navLoading, settings, categoryItems, hasTables, hasActivities, hasPlayers, hasArticles, hasVideos, hasGallery, galleryLabel]);
|
||||
|
||||
// Split navigation into visible and overflow for desktop
|
||||
const navSplit = useMemo(() => {
|
||||
const links = NAV_LINKS;
|
||||
let maxVisible = 8;
|
||||
const w = windowWidth;
|
||||
if (w >= 1600) maxVisible = 10;
|
||||
else if (w >= 1400) maxVisible = 9;
|
||||
else if (w >= 1200) maxVisible = 8;
|
||||
else if (w >= 1100) maxVisible = 7;
|
||||
else maxVisible = 6;
|
||||
if (links.length <= maxVisible) return { visible: links, overflow: [] as NavLink[] };
|
||||
const visibleCount = Math.max(1, maxVisible - 1); // reserve one slot for "Další"
|
||||
return { visible: links.slice(0, visibleCount), overflow: links.slice(visibleCount) };
|
||||
}, [NAV_LINKS, windowWidth]);
|
||||
|
||||
const moreItems = useMemo(() => {
|
||||
const out: { label: string; to: string }[] = [];
|
||||
navSplit.overflow.forEach((nav) => {
|
||||
if (nav.to) out.push({ label: nav.label, to: nav.to });
|
||||
if (Array.isArray(nav.items)) {
|
||||
nav.items.forEach((it) => out.push({ label: it.label, to: it.to }));
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}, [navSplit]);
|
||||
|
||||
return (
|
||||
<Box position="sticky" top={0} zIndex={1000}>
|
||||
{/* Top bar with socials and quick external links */}
|
||||
@@ -722,7 +789,7 @@ const Navbar: React.FC<{ fullWidth?: boolean }> = ({ fullWidth = false }) => {
|
||||
</HStack>
|
||||
{/* Desktop navigation with hover dropdowns */}
|
||||
<HStack as="nav" spacing={1} display={{ base: 'none', lg: 'flex' }} ml={4}>
|
||||
{NAV_LINKS.map((nav) => {
|
||||
{navSplit.visible.map((nav) => {
|
||||
const commonProps = {
|
||||
variant: 'ghost' as const,
|
||||
size: 'sm' as const,
|
||||
@@ -754,6 +821,9 @@ const Navbar: React.FC<{ fullWidth?: boolean }> = ({ fullWidth = false }) => {
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{navSplit.overflow.length > 0 && (
|
||||
<HoverMenu key="more" label="Další" items={moreItems} />
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
@@ -930,11 +1000,21 @@ const HoverMenu = ({ label, items, isActive }: { label: string; items: { label:
|
||||
{label}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{items.map((it) => (
|
||||
<MenuItem as={RouterLink} to={it.to} key={it.to}>
|
||||
{it.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
{items.map((it) => {
|
||||
const isExternal = /^https?:\/\//i.test(it.to);
|
||||
if (isExternal) {
|
||||
return (
|
||||
<MenuItem as="a" href={it.to} key={it.to} target="_blank" rel="noreferrer">
|
||||
{it.label}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MenuItem as={RouterLink} to={it.to} key={it.to}>
|
||||
{it.label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
@@ -5,8 +5,10 @@ import { assetUrl } from '../../utils/url';
|
||||
export interface Banner {
|
||||
id: number | string;
|
||||
name: string;
|
||||
image: string;
|
||||
image?: string;
|
||||
image_url?: string;
|
||||
url?: string;
|
||||
click_url?: string;
|
||||
placement?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -15,7 +17,7 @@ export interface Banner {
|
||||
|
||||
interface BannerDisplayProps {
|
||||
banners: Banner[];
|
||||
placement: 'homepage_top' | 'homepage_middle' | 'homepage_sidebar' | 'homepage_footer' | 'article_inline';
|
||||
placement: 'homepage_top' | 'homepage_middle' | 'homepage_sidebar' | 'homepage_footer' | 'article_inline' | 'homepage_under_table';
|
||||
containerStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
@@ -41,6 +43,8 @@ const BannerDisplay: React.FC<BannerDisplayProps> = ({ banners, placement, conta
|
||||
return 'banner-footer';
|
||||
case 'article_inline':
|
||||
return 'banner-article';
|
||||
case 'homepage_under_table':
|
||||
return 'banner-under-table';
|
||||
default:
|
||||
return 'banner';
|
||||
}
|
||||
@@ -89,6 +93,12 @@ const BannerDisplay: React.FC<BannerDisplayProps> = ({ banners, placement, conta
|
||||
display: 'block',
|
||||
margin: '24px 0',
|
||||
};
|
||||
case 'homepage_under_table':
|
||||
return {
|
||||
...base,
|
||||
margin: '12px 0 0',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
@@ -105,16 +115,16 @@ const BannerDisplay: React.FC<BannerDisplayProps> = ({ banners, placement, conta
|
||||
{activeBanners.map((banner) => (
|
||||
<ChakraLink
|
||||
key={banner.id}
|
||||
href={banner.url || '#'}
|
||||
isExternal={!!banner.url}
|
||||
target={banner.url ? '_blank' : undefined}
|
||||
rel={banner.url ? 'noopener noreferrer' : undefined}
|
||||
href={banner.url || banner.click_url || '#'}
|
||||
isExternal={!!(banner.url || banner.click_url)}
|
||||
target={(banner.url || banner.click_url) ? '_blank' : undefined}
|
||||
rel={(banner.url || banner.click_url) ? 'noopener noreferrer' : undefined}
|
||||
display="inline-block"
|
||||
_hover={{ opacity: 0.9, transform: 'translateY(-2px)' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<img
|
||||
src={assetUrl(banner.image) || banner.image}
|
||||
src={assetUrl(banner.image || banner.image_url || '') || banner.image || banner.image_url || ''}
|
||||
alt={banner.name}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
|
||||
@@ -78,6 +78,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
const onChangeRef = useRef(onChange);
|
||||
const selectedImageIdRef = useRef<string | null>(null);
|
||||
const selectImageByIdRef = useRef<(id: string) => void>(() => {});
|
||||
const toolbarDragRef = useRef<{ active: boolean; startX: number; startY: number; startLeft: number; startTop: number }>({ active: false, startX: 0, startY: 0, startLeft: 0, startTop: 0 });
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// Ensure component is mounted before rendering Quill
|
||||
@@ -401,6 +402,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let startWidth = 0;
|
||||
let rafId = 0;
|
||||
|
||||
const createResizeHandle = (img: HTMLImageElement) => {
|
||||
removeResizeHandle();
|
||||
@@ -672,23 +674,13 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
const scrollTop = editor.root.scrollTop;
|
||||
const scrollLeft = editor.root.scrollLeft;
|
||||
|
||||
// Position toolbar to the right of the image, or left if not enough space
|
||||
// Place toolbar close to the image (top-right corner inside the image area when possible)
|
||||
const toolbarWidth = 380;
|
||||
const spaceOnRight = window.innerWidth - rect.right;
|
||||
const positionRight = spaceOnRight > toolbarWidth + 20;
|
||||
|
||||
let leftPos = positionRight
|
||||
? rect.right - editorRect.left + scrollLeft + 10
|
||||
: rect.left - editorRect.left + scrollLeft - toolbarWidth - 10;
|
||||
|
||||
// Ensure toolbar is visible horizontally
|
||||
leftPos = Math.max(10, Math.min(leftPos, editorRect.width - toolbarWidth - 10));
|
||||
|
||||
// Position vertically aligned with top of image
|
||||
let topPos = rect.top - editorRect.top + scrollTop;
|
||||
|
||||
// Ensure toolbar is visible vertically
|
||||
topPos = Math.max(10, topPos);
|
||||
const margin = 8;
|
||||
let leftPos = rect.left - editorRect.left + scrollLeft + (rect.width > toolbarWidth + margin ? (rect.width - toolbarWidth - margin) : margin);
|
||||
leftPos = Math.max(margin, Math.min(leftPos, editorRect.width - toolbarWidth - margin));
|
||||
let topPos = rect.top - editorRect.top + scrollTop + margin;
|
||||
topPos = Math.max(margin, topPos);
|
||||
|
||||
setToolbarPosition({
|
||||
top: topPos,
|
||||
@@ -895,16 +887,18 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
|
||||
// Handle scroll to update resize handle position
|
||||
const handleScroll = () => {
|
||||
if (selectedImage && resizeHandle) {
|
||||
const rect = selectedImage.getBoundingClientRect();
|
||||
if (!selectedImage || !resizeHandle) return;
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const rect = selectedImage!.getBoundingClientRect();
|
||||
const editorRect = editor.root.getBoundingClientRect();
|
||||
const scrollTop = editor.root.scrollTop;
|
||||
const scrollLeft = editor.root.scrollLeft;
|
||||
resizeHandle.style.left = `${rect.left - editorRect.left + scrollLeft}px`;
|
||||
resizeHandle.style.top = `${rect.top - editorRect.top + scrollTop}px`;
|
||||
resizeHandle.style.width = `${rect.width}px`;
|
||||
resizeHandle.style.height = `${rect.height}px`;
|
||||
}
|
||||
resizeHandle!.style.left = `${rect.left - editorRect.left + scrollLeft}px`;
|
||||
resizeHandle!.style.top = `${rect.top - editorRect.top + scrollTop}px`;
|
||||
resizeHandle!.style.width = `${rect.width}px`;
|
||||
resizeHandle!.style.height = `${rect.height}px`;
|
||||
});
|
||||
};
|
||||
|
||||
// Prevent default drag behavior on images
|
||||
@@ -934,6 +928,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('resize', handleScroll);
|
||||
document.removeEventListener('scroll', handleScroll, true);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
removeResizeHandle();
|
||||
deselectImage();
|
||||
};
|
||||
@@ -1047,8 +1042,6 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
const editor = quillRef.current?.getEditor();
|
||||
if (editor) {
|
||||
onChangeRef.current(editor.root.innerHTML);
|
||||
// Force overlay reposition
|
||||
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
|
||||
}
|
||||
reselectAfterContentUpdate();
|
||||
|
||||
@@ -1111,7 +1104,6 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
setManualWidth(finalWidth.toString());
|
||||
if (editor) {
|
||||
onChangeRef.current(editor.root.innerHTML);
|
||||
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
|
||||
}
|
||||
// Keep selection active for subsequent operations (e.g., 50% → 75%)
|
||||
reselectAfterContentUpdate();
|
||||
@@ -1132,7 +1124,6 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
setManualWidth('');
|
||||
if (editor) {
|
||||
onChangeRef.current(editor.root.innerHTML);
|
||||
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
|
||||
}
|
||||
reselectAfterContentUpdate();
|
||||
toast({ title: 'Šířka resetována', status: 'info', duration: 1200 });
|
||||
@@ -1190,7 +1181,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
let cleaned = DOMPurify.sanitize(content, {
|
||||
USE_PROFILES: { html: true },
|
||||
ADD_TAGS: ['iframe'],
|
||||
ADD_ATTR: ['target', 'rel', 'allow', 'allowfullscreen', 'style', 'data-filters'],
|
||||
ADD_ATTR: ['target', 'rel', 'allow', 'allowfullscreen', 'style', 'data-filters', 'data-img-id'],
|
||||
});
|
||||
|
||||
// Replace white and very light colors with dark colors for visibility
|
||||
@@ -1236,7 +1227,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
overflow="visible"
|
||||
bg={bgColor}
|
||||
sx={{
|
||||
'.ql-toolbar': {
|
||||
@@ -1477,7 +1468,33 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{/* Toolbar Header */}
|
||||
<HStack justify="space-between">
|
||||
<HStack
|
||||
justify="space-between"
|
||||
onMouseDown={(e) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toolbarDragRef.current.active = true;
|
||||
toolbarDragRef.current.startX = e.clientX;
|
||||
toolbarDragRef.current.startY = e.clientY;
|
||||
toolbarDragRef.current.startLeft = toolbarPosition.left;
|
||||
toolbarDragRef.current.startTop = toolbarPosition.top;
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!toolbarDragRef.current.active) return;
|
||||
const dx = ev.clientX - toolbarDragRef.current.startX;
|
||||
const dy = ev.clientY - toolbarDragRef.current.startY;
|
||||
setToolbarPosition((pos) => ({ top: Math.max(0, toolbarDragRef.current.startTop + dy), left: Math.max(0, toolbarDragRef.current.startLeft + dx) }));
|
||||
};
|
||||
const onUp = () => {
|
||||
toolbarDragRef.current.active = false;
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}}
|
||||
cursor="move"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Settings size={16} />
|
||||
<Text fontWeight="bold" fontSize="sm">Úprava obrázku</Text>
|
||||
|
||||
@@ -105,7 +105,7 @@ import { DEFAULT_HOMEPAGE_ELEMENTS, HOMEPAGE_IMPLEMENTED_ELEMENTS } from '../../
|
||||
|
||||
const SUPPORTED_HOME_VARIANTS: Record<string, string[]> = {
|
||||
hero: ['grid', 'scroller', 'swiper', 'swiper_full'],
|
||||
news: ['grid', 'scroller'],
|
||||
news: ['grid_one', 'grid_two', 'grid', 'scroller'],
|
||||
matches: ['compact'],
|
||||
sponsors: ['grid', 'slider', 'scroller', 'pyramid'],
|
||||
gallery: ['grid'],
|
||||
@@ -149,7 +149,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
const [dragOverElement, setDragOverElement] = useState<string | null>(null);
|
||||
const [viewport] = useState<'desktop'>('desktop');
|
||||
const [elementStyles, setElementStyles] = useState<Record<string, any>>({});
|
||||
const [showStylePanel, setShowStylePanel] = useState(true);
|
||||
const [showStylePanel, setShowStylePanel] = useState(false);
|
||||
const [stylePanelRight, setStylePanelRight] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||
@@ -381,7 +381,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
// Auto-open Layers panel on the left by default when entering edit mode
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
setShowLayersPanel(true);
|
||||
setShowLayersPanel(false);
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
@@ -561,7 +561,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
if (showElementPicker) setShowElementPicker(false);
|
||||
else if (showLayersPanel) setShowLayersPanel(false);
|
||||
else if (selectedElement) setSelectedElement(null);
|
||||
else setIsEditing(false);
|
||||
else handleExitEditing();
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's' && hasChanges) {
|
||||
e.preventDefault();
|
||||
@@ -1515,7 +1515,9 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
display_order: index,
|
||||
settings: {
|
||||
...(configs.find(c => c.element_name === elementName)?.settings || {}),
|
||||
customCSS: (elementStyles[elementName]?.customCSS || ''),
|
||||
// Persist full styles object and custom CSS
|
||||
styles: (elementStyles[elementName] || (configs.find(c => c.element_name === elementName)?.settings as any)?.styles || {}),
|
||||
customCSS: (elementStyles[elementName]?.customCSS ?? (configs.find(c => c.element_name === elementName)?.settings as any)?.customCSS ?? ''),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1565,6 +1567,14 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
[selectedElement, localChanges, normalizeVariant]
|
||||
);
|
||||
|
||||
const handleExitEditing = useCallback(() => {
|
||||
try { document.body.classList.remove('myuibrix-edit-mode'); } catch {}
|
||||
setIsEditing(false);
|
||||
setTimeout(() => {
|
||||
try { window.location.reload(); } catch {}
|
||||
}, 120);
|
||||
}, []);
|
||||
|
||||
// Calculate viewport width - USE REAL DEVICE WIDTHS WITHOUT SCALING
|
||||
const getViewportConfig = useCallback(() => ({
|
||||
width: '100%',
|
||||
@@ -1887,6 +1897,14 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
onClick={() => setStylePanelRight(!stylePanelRight)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="whiteAlpha"
|
||||
onClick={handleStartBlank}
|
||||
>
|
||||
Začít s prázdnou stránkou
|
||||
</Button>
|
||||
{unsavedCount > 0 && (
|
||||
<Badge
|
||||
bg="yellow.400"
|
||||
@@ -1936,7 +1954,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="whiteAlpha"
|
||||
onClick={() => setIsEditing(false)}
|
||||
onClick={handleExitEditing}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
@@ -1946,10 +1964,10 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
<Box
|
||||
className="myuibrix-panel"
|
||||
position="fixed"
|
||||
left={stylePanelRight ? undefined : 4}
|
||||
right={stylePanelRight ? 4 : undefined}
|
||||
top={64}
|
||||
bottom={4}
|
||||
left={stylePanelRight ? undefined : 0}
|
||||
right={stylePanelRight ? 0 : undefined}
|
||||
top={0}
|
||||
bottom={0}
|
||||
width="380px"
|
||||
zIndex={9998}
|
||||
overflow="hidden"
|
||||
@@ -1958,7 +1976,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
fontFamily="var(--chakra-fonts-body)"
|
||||
bg="rgba(255, 255, 255, 0.95)"
|
||||
backdropFilter="blur(12px) saturate(180%)"
|
||||
borderRadius="2xl"
|
||||
borderRadius="0"
|
||||
boxShadow="0 20px 60px rgba(0,0,0,0.3), 0 8px 24px rgba(0,0,0,0.2)"
|
||||
border="1px solid rgba(255,255,255,0.3)"
|
||||
sx={{
|
||||
@@ -1979,7 +1997,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
flexShrink={0}
|
||||
borderTopRadius="2xl"
|
||||
borderTopRadius="0"
|
||||
boxShadow="0 2px 8px rgba(0,0,0,0.1)"
|
||||
borderBottom="1px solid rgba(255,255,255,0.2)"
|
||||
>
|
||||
@@ -2040,11 +2058,10 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
colorScheme={isEditing ? "red" : "whiteAlpha"}
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
setIsEditing(!isEditing);
|
||||
if (isEditing) {
|
||||
setSelectedElement(null);
|
||||
setShowLayersPanel(false);
|
||||
setShowElementPicker(false);
|
||||
handleExitEditing();
|
||||
} else {
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
borderRadius="full"
|
||||
@@ -2357,7 +2374,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
)}
|
||||
|
||||
{/* Unsaved Changes Indicator */}
|
||||
{isEditing && hasChanges && (
|
||||
{false && isEditing && hasChanges && (
|
||||
<Box
|
||||
position="fixed"
|
||||
top={4}
|
||||
|
||||
@@ -91,7 +91,7 @@ const SpartaNavbar: React.FC = () => {
|
||||
|
||||
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') }));
|
||||
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(() => {
|
||||
@@ -155,6 +155,20 @@ const SpartaNavbar: React.FC = () => {
|
||||
const isActive = isPathActive(nav.to);
|
||||
const className = isActive ? 'sparta-button-tertiary' : 'sparta-button-tertiary';
|
||||
|
||||
// When categories are present under Články/Blog, render non-clickable label + 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}>
|
||||
<span className="sparta-button-tertiary" style={{ pointerEvents: 'none', opacity: 0.9 }}>{nav.label}</span>
|
||||
{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)}>
|
||||
|
||||
@@ -7,7 +7,7 @@ export type ClubHeroTopbarVariant = 'brand' | 'minimal' | 'badge';
|
||||
|
||||
const cls = (...parts: Array<string | false | null | undefined>) => parts.filter(Boolean).join(' ');
|
||||
|
||||
const ClubHeroTopbar: React.FC<{ variant?: ClubHeroTopbarVariant; fullBleed?: boolean }>= ({ variant = 'brand', fullBleed = false }) => {
|
||||
const ClubHeroTopbar: React.FC<{ variant?: ClubHeroTopbarVariant; fullBleed?: boolean }>= ({ variant = 'minimal', fullBleed = false }) => {
|
||||
const { data: settings } = usePublicSettings();
|
||||
const theme = useClubTheme();
|
||||
const title = settings?.club_name || theme.name || 'Fotbalový klub';
|
||||
@@ -31,12 +31,14 @@ const ClubHeroTopbar: React.FC<{ variant?: ClubHeroTopbarVariant; fullBleed?: bo
|
||||
<div className="club-hero-topbar__tagline">{tagline}</div>
|
||||
</div>
|
||||
<div className="club-hero-topbar__spacer" />
|
||||
<div className="club-hero-topbar__actions">
|
||||
<a href={calendarUrl} className="sparta-button-tertiary">Kalendář</a>
|
||||
{shopUrl && (
|
||||
<a href={shopUrl} target="_blank" rel="noreferrer" className="sparta-button-primary">Fanshop</a>
|
||||
)}
|
||||
</div>
|
||||
{variant !== 'minimal' && (
|
||||
<div className="club-hero-topbar__actions">
|
||||
<a href={calendarUrl} className="sparta-button-tertiary">Kalendář</a>
|
||||
{shopUrl && (
|
||||
<a href={shopUrl} target="_blank" rel="noreferrer" className="sparta-button-primary">Fanshop</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -110,6 +110,8 @@ const ContactMap: React.FC<ContactMapProps> = ({
|
||||
}) => {
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstanceRef = useRef<any>(null);
|
||||
const tileLayerRef = useRef<any>(null);
|
||||
const markerRef = useRef<any>(null);
|
||||
const [isLoaded, setIsLoaded] = React.useState(false);
|
||||
const [loadError, setLoadError] = React.useState<string | null>(null);
|
||||
|
||||
@@ -175,52 +177,38 @@ const ContactMap: React.FC<ContactMapProps> = ({
|
||||
|
||||
mapInstanceRef.current = map;
|
||||
|
||||
// Get tile layer URL based on style
|
||||
let tileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
let attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
// Initial tile layer
|
||||
const { tileUrl, attribution } = (() => {
|
||||
let url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
let attr = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
if (mapStyle && MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]) {
|
||||
const style = MAP_STYLES[mapStyle as keyof typeof MAP_STYLES];
|
||||
url = style.url;
|
||||
attr = style.attribution;
|
||||
} else if (mapStyle && mapStyle.startsWith('http')) {
|
||||
url = mapStyle;
|
||||
}
|
||||
return { tileUrl: url, attribution: attr };
|
||||
})();
|
||||
|
||||
// Use predefined styles or custom URL
|
||||
if (mapStyle && MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]) {
|
||||
const style = MAP_STYLES[mapStyle as keyof typeof MAP_STYLES];
|
||||
tileUrl = style.url;
|
||||
attribution = style.attribution;
|
||||
} else if (mapStyle && mapStyle.startsWith('http')) {
|
||||
// Custom tile URL
|
||||
tileUrl = mapStyle;
|
||||
}
|
||||
|
||||
// Add tile layer
|
||||
const tileLayer = L.tileLayer(tileUrl, {
|
||||
attribution: attribution,
|
||||
tileLayerRef.current = L.tileLayer(tileUrl, {
|
||||
attribution,
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
// Apply club color overlay if provided
|
||||
if (clubPrimaryColor && clubPrimaryColor !== '') {
|
||||
const colorFilter = createColorFilter(clubPrimaryColor);
|
||||
if (colorFilter) {
|
||||
const pane = map.createPane('colorOverlay');
|
||||
pane.style.zIndex = '400';
|
||||
pane.style.pointerEvents = 'none';
|
||||
pane.style.mixBlendMode = 'multiply';
|
||||
pane.style.backgroundColor = colorFilter;
|
||||
pane.style.opacity = '0.15';
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom marker icon with club colors
|
||||
const markerColor = clubPrimaryColor || '#3388ff';
|
||||
const customIcon = createCustomMarkerIcon(markerColor, L);
|
||||
|
||||
// Add marker
|
||||
const marker = L.marker([latitude, longitude], { icon: customIcon }).addTo(map);
|
||||
markerRef.current = L.marker([latitude, longitude], { icon: customIcon }).addTo(map);
|
||||
|
||||
// Add popup if address is provided
|
||||
if (clubName || address) {
|
||||
let popupContent = '';
|
||||
if (clubName) popupContent += `<b>${clubName}</b><br>`;
|
||||
if (address) popupContent += address;
|
||||
marker.bindPopup(popupContent);
|
||||
markerRef.current.bindPopup(popupContent);
|
||||
}
|
||||
|
||||
// Enable scroll zoom on click
|
||||
@@ -238,14 +226,71 @@ const ContactMap: React.FC<ContactMapProps> = ({
|
||||
setLoadError('Failed to initialize map');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (mapInstanceRef.current) {
|
||||
mapInstanceRef.current.remove();
|
||||
try {
|
||||
if (mapInstanceRef.current) {
|
||||
mapInstanceRef.current.remove();
|
||||
}
|
||||
} finally {
|
||||
mapInstanceRef.current = null;
|
||||
tileLayerRef.current = null;
|
||||
markerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isLoaded, latitude, longitude, zoom, address, clubName, mapStyle, clubPrimaryColor, clubSecondaryColor]);
|
||||
}, [isLoaded]);
|
||||
|
||||
// Update center/zoom and marker when coords/zoom change
|
||||
useEffect(() => {
|
||||
if (!mapInstanceRef.current || !L) return;
|
||||
try {
|
||||
const map = mapInstanceRef.current;
|
||||
if (typeof latitude === 'number' && typeof longitude === 'number') {
|
||||
map.setView([latitude, longitude], typeof zoom === 'number' ? zoom : map.getZoom());
|
||||
if (markerRef.current) {
|
||||
markerRef.current.setLatLng([latitude, longitude]);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}, [latitude, longitude, zoom]);
|
||||
|
||||
// Update map style (tile layer) when mapStyle changes
|
||||
useEffect(() => {
|
||||
if (!mapInstanceRef.current || !L) return;
|
||||
try {
|
||||
const map = mapInstanceRef.current;
|
||||
// Compute URL and attribution
|
||||
let url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
let attr = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
if (mapStyle && MAP_STYLES[mapStyle as keyof typeof MAP_STYLES]) {
|
||||
const style = MAP_STYLES[mapStyle as keyof typeof MAP_STYLES];
|
||||
url = style.url;
|
||||
attr = style.attribution;
|
||||
} else if (mapStyle && mapStyle.startsWith('http')) {
|
||||
url = mapStyle;
|
||||
}
|
||||
if (tileLayerRef.current) {
|
||||
map.removeLayer(tileLayerRef.current);
|
||||
}
|
||||
tileLayerRef.current = L.tileLayer(url, { attribution: attr, maxZoom: 19 }).addTo(map);
|
||||
} catch {}
|
||||
}, [mapStyle]);
|
||||
|
||||
// Update popup content when clubName/address change
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (markerRef.current) {
|
||||
if (clubName || address) {
|
||||
let popupContent = '';
|
||||
if (clubName) popupContent += `<b>${clubName}</b><br>`;
|
||||
if (address) popupContent += address;
|
||||
markerRef.current.bindPopup(popupContent);
|
||||
} else {
|
||||
markerRef.current.unbindPopup();
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}, [clubName, address]);
|
||||
|
||||
// Helper function to create color filter
|
||||
function createColorFilter(color: string): string | null {
|
||||
|
||||
@@ -17,6 +17,11 @@ const TeamScroller: React.FC = () => {
|
||||
<Image src={assetUrl(p.image_url) || '/logo192.png'} alt={p.first_name + ' ' + p.last_name} w="140px" h="140px" objectFit="cover" borderRadius="lg" fallbackSrc="/dist/img/logo-club-empty.svg" />
|
||||
<Text fontWeight="bold" textAlign="center">{p.first_name} {p.last_name}</Text>
|
||||
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.400')}>{p.position}</Text>
|
||||
{p.date_of_birth ? (
|
||||
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.400')}>
|
||||
Věk: {calculateAge(p.date_of_birth)} let
|
||||
</Text>
|
||||
) : null}
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
@@ -24,4 +29,18 @@ const TeamScroller: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function calculateAge(dob: string): number | null {
|
||||
try {
|
||||
const d = new Date(dob);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - d.getFullYear();
|
||||
const m = today.getMonth() - d.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < d.getDate())) age--;
|
||||
return age;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default TeamScroller;
|
||||
|
||||
@@ -10,9 +10,10 @@ import SponsorsSection from '../common/SponsorsSection';
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode;
|
||||
headerInsideContainer?: boolean;
|
||||
showSponsorsSection?: boolean;
|
||||
}
|
||||
|
||||
export const MainLayout: React.FC<MainLayoutProps> = ({ children, headerInsideContainer = false }) => {
|
||||
export const MainLayout: React.FC<MainLayoutProps> = ({ children, headerInsideContainer = false, showSponsorsSection = true }) => {
|
||||
const [showTop, setShowTop] = useState(false);
|
||||
const { getStyles, getVariant } = useAllPageElementConfigs('homepage');
|
||||
const headerVariant = getVariant('header', 'unified');
|
||||
@@ -54,9 +55,11 @@ export const MainLayout: React.FC<MainLayoutProps> = ({ children, headerInsideCo
|
||||
</Box>
|
||||
{children}
|
||||
</Container>
|
||||
<Box data-element="sponsors" data-variant={sponsorsVariant} style={{ ...getStyles('sponsors') }}>
|
||||
<SponsorsSection />
|
||||
</Box>
|
||||
{showSponsorsSection && (
|
||||
<Box data-element="sponsors" data-variant={sponsorsVariant} style={{ ...getStyles('sponsors') }}>
|
||||
<SponsorsSection />
|
||||
</Box>
|
||||
)}
|
||||
<Box as="footer" data-element="footer" data-variant={footerVariant} style={{ ...getStyles('footer') }}>
|
||||
<Footer />
|
||||
</Box>
|
||||
@@ -74,9 +77,11 @@ export const MainLayout: React.FC<MainLayoutProps> = ({ children, headerInsideCo
|
||||
{children}
|
||||
</Container>
|
||||
{/* Global sponsors section across front-facing pages */}
|
||||
<Box data-element="sponsors" data-variant={sponsorsVariant} style={{ ...getStyles('sponsors') }}>
|
||||
<SponsorsSection />
|
||||
</Box>
|
||||
{showSponsorsSection && (
|
||||
<Box data-element="sponsors" data-variant={sponsorsVariant} style={{ ...getStyles('sponsors') }}>
|
||||
<SponsorsSection />
|
||||
</Box>
|
||||
)}
|
||||
<Box as="footer" data-element="footer" data-variant={footerVariant} style={{ ...getStyles('footer') }}>
|
||||
<Footer />
|
||||
</Box>
|
||||
|
||||
@@ -63,15 +63,13 @@ export default function NewsletterSubscribe() {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cardBg = useColorModeValue('white', 'transparent');
|
||||
const cardBorder = useColorModeValue('gray.200', 'gray.600');
|
||||
|
||||
const headingColor = useColorModeValue('gray.800', 'white');
|
||||
const textColor = useColorModeValue('gray.600', 'gray.300');
|
||||
const disclaimerColor = useColorModeValue('gray.500', 'gray.400');
|
||||
|
||||
return (
|
||||
<Box w="100%" maxW="xl" mx="auto" p={4} bg={cardBg} borderRadius="md" boxShadow="sm" borderWidth="1px" borderColor={cardBorder}>
|
||||
<Box w="100%" maxW="xl" mx="auto" p={4}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
<Text fontSize="xl" fontWeight="bold" textAlign="center" color={headingColor}>
|
||||
Přihlaste se k odběru novinek
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { assetUrl, sanitizeClubName } from '../../utils/url';
|
||||
|
||||
export interface StandingRow {
|
||||
position?: number;
|
||||
@@ -21,7 +22,7 @@ export interface StandingRow {
|
||||
score?: string;
|
||||
}
|
||||
|
||||
const StandingsCard: React.FC<{ rows: StandingRow[]; onRowClick?: (row: StandingRow, index: number) => void }>= ({ rows, onRowClick }) => {
|
||||
const StandingsCard: React.FC<{ rows: StandingRow[]; onRowClick?: (row: StandingRow, index: number) => void; variant?: 'logos' | 'plain' }>= ({ rows, onRowClick, variant = 'logos' }) => {
|
||||
const safe = Array.isArray(rows) ? rows : [];
|
||||
return (
|
||||
<div className="table-card">
|
||||
@@ -40,36 +41,65 @@ const StandingsCard: React.FC<{ rows: StandingRow[]; onRowClick?: (row: Standing
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{safe.slice(0, 8).map((row, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
onClick={() => onRowClick?.(row, idx)}
|
||||
style={{
|
||||
cursor: onRowClick ? 'pointer' : 'default',
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--card-border)',
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLTableRowElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.08)';
|
||||
(e.currentTarget as HTMLTableRowElement).style.borderColor = 'var(--primary)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLTableRowElement).style.boxShadow = 'none';
|
||||
(e.currentTarget as HTMLTableRowElement).style.borderColor = 'var(--card-border)';
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: '10px 8px', fontWeight: 700, color: 'var(--secondary)' }}>{row.position ?? row.pos ?? row.rank ?? idx + 1}</td>
|
||||
<td style={{ padding: '10px 8px', fontWeight: 600 }}>{(row as any).team?.name ?? (row as any).team ?? (row as any).club ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).played ?? (row as any).matches ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).wins ?? (row as any).win ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).draws ?? (row as any).draw ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).losses ?? (row as any).loss ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center', display: 'none' }} className="hide-mobile">{(row as any).score ?? '-'}</td>
|
||||
<td style={{ padding: '10px 8px', textAlign: 'center', fontWeight: 800 }}>{(row as any).points ?? (row as any).pts ?? '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
{safe.slice(0, 8).map((row, idx) => {
|
||||
const teamNameRaw = (row as any).team?.name ?? (row as any).team ?? (row as any).club ?? '-';
|
||||
const teamName = sanitizeClubName(teamNameRaw);
|
||||
const logo = (row as any).team_logo_url;
|
||||
const logoSrc = logo ? (assetUrl(logo) || logo) : null;
|
||||
return (
|
||||
<tr
|
||||
key={idx}
|
||||
onClick={() => onRowClick?.(row, idx)}
|
||||
style={{
|
||||
cursor: onRowClick ? 'pointer' : 'default',
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--card-border)',
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLTableRowElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.08)';
|
||||
(e.currentTarget as HTMLTableRowElement).style.borderColor = 'var(--primary)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLTableRowElement).style.boxShadow = 'none';
|
||||
(e.currentTarget as HTMLTableRowElement).style.borderColor = 'var(--card-border)';
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: '10px 8px', fontWeight: 700, color: 'var(--secondary)' }}>{row.position ?? row.pos ?? row.rank ?? idx + 1}</td>
|
||||
<td style={{ padding: '10px 8px', fontWeight: 600 }}>
|
||||
{variant === 'logos' ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
||||
{logoSrc ? (
|
||||
<img
|
||||
src={logoSrc as string}
|
||||
alt={teamName || 'Tým'}
|
||||
loading="lazy"
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
background: 'var(--bg-soft)',
|
||||
border: '1px solid var(--card-border)',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{teamName}</span>
|
||||
</span>
|
||||
) : (
|
||||
teamNameRaw
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).played ?? (row as any).matches ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).wins ?? (row as any).win ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).draws ?? (row as any).draw ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center' }}>{(row as any).losses ?? (row as any).loss ?? '-'}</td>
|
||||
<td style={{ padding: '10px 4px', textAlign: 'center', display: 'none' }} className="hide-mobile">{(row as any).score ?? '-'}</td>
|
||||
<td style={{ padding: '10px 8px', textAlign: 'center', fontWeight: 800 }}>{(row as any).points ?? (row as any).pts ?? '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user