This commit is contained in:
Tomáš Dvořák
2025-10-16 17:10:13 +02:00
parent f5e7be92c7
commit 35d0954afd
84 changed files with 9571 additions and 4668 deletions
+143 -21
View File
@@ -44,6 +44,11 @@ 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';
import { getEvents } from '../services/eventService';
import { getPlayers } from '../services/public';
import { getArticles } from '../services/articles';
import { getCachedYouTube } from '../services/youtube';
import { getZoneramaManifestWithFallbacks } from '../services/zonerama';
type NavLink = { label: string; to?: string; items?: { label: string; to: string }[]; external?: boolean };
@@ -72,7 +77,7 @@ const normalizeSocialUrl = (network: 'facebook' | 'instagram' | 'youtube', raw?:
};
// Mobile menu component
const MobileMenu = ({ isOpen, onClose, isAdmin, menuBg, dividerColor, settings, categories, galleryHref, galleryLabel, hasTables, dynamicNavItems, navLoading }: {
const MobileMenu = ({ isOpen, onClose, isAdmin, menuBg, dividerColor, settings, categories, galleryHref, galleryLabel, hasTables, hasActivities, hasPlayers, hasArticles, hasVideos, hasGallery, dynamicNavItems, navLoading }: {
isOpen: boolean;
onClose: () => void;
isAdmin: boolean;
@@ -83,6 +88,11 @@ const MobileMenu = ({ isOpen, onClose, isAdmin, menuBg, dividerColor, settings,
galleryHref?: string | null;
galleryLabel?: string;
hasTables?: boolean | null;
hasActivities?: boolean | null;
hasPlayers?: boolean | null;
hasArticles?: boolean | null;
hasVideos?: boolean | null;
hasGallery?: boolean | null;
dynamicNavItems: NavigationItem[];
navLoading: boolean;
}) => (
@@ -150,8 +160,12 @@ const MobileMenu = ({ isOpen, onClose, isAdmin, menuBg, dividerColor, settings,
)}
<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>
{hasActivities !== false && (
<Button as={RouterLink} to="/aktivity" variant="ghost" justifyContent="flex-start">Aktivity</Button>
)}
{hasPlayers !== false && (
<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}
@@ -173,24 +187,32 @@ const MobileMenu = ({ isOpen, onClose, isAdmin, menuBg, dividerColor, settings,
</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>
{hasArticles !== false && (
<>
<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>
)}
</>
)}
{hasVideos !== false && (
<Button as={RouterLink} to="/videa" variant="ghost" justifyContent="flex-start">Videa</Button>
)}
<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>
{hasGallery !== false && (
<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>
)}
@@ -232,6 +254,11 @@ const Navbar = () => {
const navTextColor = useColorModeValue('gray.700', 'gray.200');
const [scrolled, setScrolled] = useState(false);
const [hasTables, setHasTables] = useState<boolean | null>(null);
const [hasActivities, setHasActivities] = useState<boolean | null>(null);
const [hasPlayers, setHasPlayers] = useState<boolean | null>(null);
const [hasArticles, setHasArticles] = useState<boolean | null>(null);
const [hasVideos, setHasVideos] = useState<boolean | null>(null);
const [hasGallery, setHasGallery] = useState<boolean | null>(null);
const [dynamicNavItems, setDynamicNavItems] = useState<NavigationItem[]>([]);
const [navLoading, setNavLoading] = useState(true);
@@ -381,6 +408,76 @@ const Navbar = () => {
return () => { disposed = true; };
}, []);
// Determine if there are any activities/events available
useEffect(() => {
let disposed = false;
(async () => {
try {
const events = await getEvents();
if (!disposed) setHasActivities(Array.isArray(events) && events.length > 0);
} catch {
if (!disposed) setHasActivities(false);
}
})();
return () => { disposed = true; };
}, []);
// Determine if there are any players available
useEffect(() => {
let disposed = false;
(async () => {
try {
const players = await getPlayers();
if (!disposed) setHasPlayers(Array.isArray(players) && players.length > 0);
} catch {
if (!disposed) setHasPlayers(false);
}
})();
return () => { disposed = true; };
}, []);
// Determine if there are any articles available
useEffect(() => {
let disposed = false;
(async () => {
try {
const result = await getArticles({ page: 1, page_size: 1, published: true });
if (!disposed) setHasArticles(result.total > 0);
} catch {
if (!disposed) setHasArticles(false);
}
})();
return () => { disposed = true; };
}, []);
// Determine if there are any videos available
useEffect(() => {
let disposed = false;
(async () => {
try {
const youtube = await getCachedYouTube();
if (!disposed) setHasVideos(youtube && Array.isArray(youtube.videos) && youtube.videos.length > 0);
} catch {
if (!disposed) setHasVideos(false);
}
})();
return () => { disposed = true; };
}, []);
// Determine if there is any gallery content available
useEffect(() => {
let disposed = false;
(async () => {
try {
const manifest = await getZoneramaManifestWithFallbacks();
if (!disposed) setHasGallery(Array.isArray(manifest) && manifest.length > 0);
} catch {
if (!disposed) setHasGallery(false);
}
})();
return () => { disposed = true; };
}, []);
const isPathActive = (to?: string) => {
if (!to) return false;
// Active when current pathname starts with target (handles subroutes)
@@ -459,8 +556,33 @@ const Navbar = () => {
links = links.filter((n) => n.label !== 'Tabulky');
}
// Hide Aktivity when there are no activities
if (hasActivities === false) {
links = links.filter((n) => n.label !== 'Aktivity');
}
// Hide Hráči when there are no players
if (hasPlayers === false) {
links = links.filter((n) => n.label !== 'Hráči');
}
// Hide Články when there are no articles
if (hasArticles === false) {
links = links.filter((n) => n.label !== 'Články');
}
// Hide Videa when there are no videos
if (hasVideos === false) {
links = links.filter((n) => n.label !== 'Videa');
}
// Hide Fotogalerie when there is no gallery content
if (hasGallery === false) {
links = links.filter((n) => n.label === galleryLabel).length === 0 ? links : links.filter((n) => n.label !== galleryLabel);
}
return links;
}, [dynamicNavItems, navLoading, settings, categoryItems, hasTables, galleryLabel]);
}, [dynamicNavItems, navLoading, settings, categoryItems, hasTables, hasActivities, hasPlayers, hasArticles, hasVideos, hasGallery, galleryLabel]);
return (
<Box position="sticky" top={0} zIndex={1000}>
@@ -501,7 +623,7 @@ const Navbar = () => {
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} />
<MobileMenu isOpen={isOpen} onClose={onClose} isAdmin={isAdmin} menuBg={menuBg} dividerColor={dividerColor} settings={settings} categories={navCategories} galleryHref={galleryHref} galleryLabel={galleryLabel} hasTables={hasTables} hasActivities={hasActivities} hasPlayers={hasPlayers} hasArticles={hasArticles} hasVideos={hasVideos} hasGallery={hasGallery} dynamicNavItems={dynamicNavItems} navLoading={navLoading} />
<Container maxW="7xl">
<Flex h={16} alignItems="center" justifyContent="space-between">
<HStack spacing={4} alignItems="center">
@@ -18,7 +18,6 @@ import {
SimpleGrid,
useToast,
VStack,
useColorModeValue,
ButtonGroup,
IconButton,
Tooltip,
@@ -82,11 +81,12 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
const [cropMaxWidth, setCropMaxWidth] = useState<number>(1500);
const imgRef = useRef<HTMLImageElement | null>(null);
const borderColor = useColorModeValue('gray.200', 'gray.600');
const bgColor = useColorModeValue('white', 'gray.800');
const hoverBg = useColorModeValue('gray.50', 'gray.700');
const toolbarBg = useColorModeValue('white', 'gray.800');
const toolbarBorder = useColorModeValue('gray.200', 'gray.700');
// Force white mode for better readability in admin
const borderColor = 'gray.200';
const bgColor = 'white';
const hoverBg = 'gray.50';
const toolbarBg = 'white';
const toolbarBorder = 'gray.200';
// Image editing state
const [selectedImageElement, setSelectedImageElement] = useState<HTMLImageElement | null>(null);
@@ -569,6 +569,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
{editorMode === 'rich' ? (
<Box
position="relative"
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
@@ -579,15 +580,27 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
borderBottom: '1px solid',
borderColor: borderColor,
bg: hoverBg,
'& button': {
color: 'gray.700 !important',
},
'& .ql-stroke': {
stroke: 'gray.700 !important',
},
'& .ql-fill': {
fill: 'gray.700 !important',
},
},
'.ql-container': {
fontSize: '16px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
bg: 'white',
},
'.ql-editor': {
minHeight: height,
maxHeight: '70vh',
overflowY: 'auto',
bg: 'white !important',
color: 'gray.800 !important',
'&::-webkit-scrollbar': {
width: '8px',
},
@@ -614,7 +627,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
},
},
'.ql-editor.ql-blank::before': {
color: 'gray.400',
color: 'gray.400 !important',
fontStyle: 'italic',
},
}}
@@ -869,7 +882,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
justifyContent="center"
alignItems="center"
p={4}
bg={useColorModeValue('gray.50', 'gray.900')}
bg="gray.50"
borderRadius="md"
>
<ReactCrop
@@ -0,0 +1,311 @@
import React, { useState } from 'react';
import {
Box,
VStack,
HStack,
IconButton,
Button,
Text,
SimpleGrid,
Tooltip,
useColorModeValue,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure,
} from '@chakra-ui/react';
import {
FiPlus,
FiColumns,
FiGrid,
FiLayout,
FiTrash2,
} from 'react-icons/fi';
import { FaColumns, FaRegNewspaper, FaThLarge } from 'react-icons/fa';
interface Column {
id: string;
width: string;
elements: string[];
}
interface ColumnLayoutManagerProps {
elementName: string;
onLayoutChange: (columns: Column[]) => void;
currentColumns?: Column[];
}
const ColumnLayoutManager: React.FC<ColumnLayoutManagerProps> = ({
elementName,
onLayoutChange,
currentColumns = [],
}) => {
const [columns, setColumns] = useState<Column[]>(currentColumns);
const { isOpen, onOpen, onClose } = useDisclosure();
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const hoverBg = useColorModeValue('gray.50', 'gray.700');
const layoutTemplates = [
{
name: 'Single Column',
icon: FiLayout,
columns: [{ id: '1', width: '100%', elements: [] }],
},
{
name: 'Two Equal',
icon: FaColumns,
columns: [
{ id: '1', width: '50%', elements: [] },
{ id: '2', width: '50%', elements: [] },
],
},
{
name: 'Three Equal',
icon: FiGrid,
columns: [
{ id: '1', width: '33.33%', elements: [] },
{ id: '2', width: '33.33%', elements: [] },
{ id: '3', width: '33.33%', elements: [] },
],
},
{
name: 'Four Equal',
icon: FaThLarge,
columns: [
{ id: '1', width: '25%', elements: [] },
{ id: '2', width: '25%', elements: [] },
{ id: '3', width: '25%', elements: [] },
{ id: '4', width: '25%', elements: [] },
],
},
{
name: 'Left Sidebar',
icon: FiColumns,
columns: [
{ id: '1', width: '33.33%', elements: [] },
{ id: '2', width: '66.67%', elements: [] },
],
},
{
name: 'Right Sidebar',
icon: FiColumns,
columns: [
{ id: '1', width: '66.67%', elements: [] },
{ id: '2', width: '33.33%', elements: [] },
],
},
{
name: 'Featured + Two',
icon: FaRegNewspaper,
columns: [
{ id: '1', width: '50%', elements: [] },
{ id: '2', width: '25%', elements: [] },
{ id: '3', width: '25%', elements: [] },
],
},
{
name: 'Three + One',
icon: FiGrid,
columns: [
{ id: '1', width: '75%', elements: [] },
{ id: '2', width: '25%', elements: [] },
],
},
];
const applyTemplate = (template: typeof layoutTemplates[0]) => {
setColumns(template.columns);
onLayoutChange(template.columns);
onClose();
};
const addColumn = () => {
const newColumn: Column = {
id: `col-${Date.now()}`,
width: `${100 / (columns.length + 1)}%`,
elements: [],
};
// Recalculate existing column widths
const newColumns = [
...columns.map(col => ({
...col,
width: `${100 / (columns.length + 1)}%`,
})),
newColumn,
];
setColumns(newColumns);
onLayoutChange(newColumns);
};
const removeColumn = (columnId: string) => {
const newColumns = columns.filter(col => col.id !== columnId);
// Recalculate remaining column widths
const equalWidth = `${100 / newColumns.length}%`;
const updatedColumns = newColumns.map(col => ({
...col,
width: equalWidth,
}));
setColumns(updatedColumns);
onLayoutChange(updatedColumns);
};
return (
<Box>
<VStack align="stretch" spacing={3}>
<HStack justify="space-between">
<Text fontSize="xs" fontWeight="bold" color="gray.500" textTransform="uppercase">
Column Layout
</Text>
<HStack spacing={2}>
<Button
size="xs"
leftIcon={<FiColumns />}
onClick={onOpen}
variant="outline"
>
Templates
</Button>
<Tooltip label="Add Column">
<IconButton
aria-label="Add column"
icon={<FiPlus />}
size="xs"
colorScheme="blue"
onClick={addColumn}
isDisabled={columns.length >= 6}
/>
</Tooltip>
</HStack>
</HStack>
{/* Current Columns Preview */}
{columns.length > 0 && (
<Box
p={3}
borderRadius="md"
border="1px"
borderColor={borderColor}
bg={bgColor}
>
<HStack spacing={2} align="stretch">
{columns.map((column, index) => (
<Box
key={column.id}
flex={column.width}
p={2}
borderRadius="sm"
border="2px dashed"
borderColor="blue.300"
bg={hoverBg}
position="relative"
minHeight="80px"
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
>
<Text fontSize="xs" fontWeight="bold" color="gray.500">
Column {index + 1}
</Text>
<Text fontSize="xs" color="gray.400">
{column.width}
</Text>
{columns.length > 1 && (
<IconButton
aria-label="Remove column"
icon={<FiTrash2 />}
size="xs"
position="absolute"
top={1}
right={1}
variant="ghost"
colorScheme="red"
onClick={() => removeColumn(column.id)}
/>
)}
<Tooltip label="Click + to add element">
<IconButton
aria-label="Add element to column"
icon={<FiPlus />}
size="xs"
position="absolute"
bottom={1}
colorScheme="green"
variant="ghost"
/>
</Tooltip>
</Box>
))}
</HStack>
</Box>
)}
</VStack>
{/* Layout Templates Modal */}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>Choose Column Layout</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<SimpleGrid columns={2} spacing={4}>
{layoutTemplates.map((template, index) => (
<Box
key={index}
p={4}
borderRadius="lg"
border="2px"
borderColor={borderColor}
cursor="pointer"
transition="all 0.2s"
_hover={{
borderColor: 'blue.400',
transform: 'translateY(-2px)',
boxShadow: 'md',
}}
onClick={() => applyTemplate(template)}
>
<VStack spacing={3}>
<Box
as={template.icon}
fontSize="2xl"
color="blue.500"
/>
<Text fontWeight="bold" fontSize="sm">
{template.name}
</Text>
{/* Visual Preview */}
<HStack spacing={1} width="100%" height="40px">
{template.columns.map((col, i) => (
<Box
key={i}
flex={col.width}
bg="blue.100"
borderRadius="sm"
height="100%"
/>
))}
</HStack>
</VStack>
</Box>
))}
</SimpleGrid>
</ModalBody>
</ModalContent>
</Modal>
</Box>
);
};
export default ColumnLayoutManager;
@@ -0,0 +1,162 @@
import React from 'react';
import {
Box,
VStack,
HStack,
Text,
Button,
Link,
Icon,
Divider,
useColorModeValue,
Badge,
} from '@chakra-ui/react';
import {
FiExternalLink,
FiSettings,
FiUsers,
FiFileText,
FiVideo,
FiImage,
FiCalendar,
FiTag,
FiShoppingCart,
FiMail,
} from 'react-icons/fi';
interface AdminLink {
label: string;
url: string;
icon: any;
description?: string;
badge?: string;
}
interface ContextualAdminLinksProps {
elementName: string;
}
const ContextualAdminLinks: React.FC<ContextualAdminLinksProps> = ({ elementName }) => {
const bgColor = useColorModeValue('blue.50', 'blue.900');
const borderColor = useColorModeValue('blue.200', 'blue.700');
const getLinksForElement = (element: string): AdminLink[] => {
const links: Record<string, AdminLink[]> = {
hero: [
{ label: 'Manage Articles', url: '/admin/articles', icon: FiFileText, description: 'Edit featured articles' },
{ label: 'Upload Images', url: '/admin/media', icon: FiImage, description: 'Manage hero images' },
],
news: [
{ label: 'Manage Articles', url: '/admin/articles', icon: FiFileText, description: 'Create and edit news' },
{ label: 'Categories', url: '/admin/categories', icon: FiTag, description: 'Organize article categories' },
{ label: 'Article Settings', url: '/admin/settings/articles', icon: FiSettings, description: 'Configure display options' },
],
matches: [
{ label: 'Manage Matches', url: '/admin/matches', icon: FiCalendar, description: 'Schedule and edit matches' },
{ label: 'Match Settings', url: '/admin/settings/matches', icon: FiSettings, description: 'Configure match display' },
],
table: [
{ label: 'Update Table', url: '/admin/table', icon: FiSettings, description: 'Refresh league standings' },
{ label: 'Team Settings', url: '/admin/settings/team', icon: FiSettings },
],
team: [
{ label: 'Manage Players', url: '/admin/team/players', icon: FiUsers, description: 'Add and edit players' },
{ label: 'Team Settings', url: '/admin/settings/team', icon: FiSettings, description: 'Configure team display' },
],
videos: [
{ label: 'Manage Videos', url: '/admin/videos', icon: FiVideo, description: 'Add YouTube videos' },
{ label: 'Video Settings', url: '/admin/settings/videos', icon: FiSettings, description: 'Configure video player' },
],
gallery: [
{ label: 'Gallery Settings', url: '/admin/settings/gallery', icon: FiImage, description: 'Set gallery URL' },
],
merch: [
{ label: 'Fanshop Settings', url: '/admin/settings/fanshop', icon: FiShoppingCart, description: 'Configure merchandise' },
],
newsletter: [
{ label: 'Newsletter Settings', url: '/admin/settings/newsletter', icon: FiMail, description: 'Email configuration' },
{ label: 'Subscribers', url: '/admin/newsletter/subscribers', icon: FiUsers, description: 'View subscribers' },
],
sponsors: [
{ label: 'Manage Sponsors', url: '/admin/sponsors', icon: FiImage, description: 'Add and edit sponsors' },
],
};
return links[element] || [
{ label: 'Admin Dashboard', url: '/admin', icon: FiSettings, description: 'Go to admin panel' },
];
};
const links = getLinksForElement(elementName);
return (
<Box>
<VStack align="stretch" spacing={3}>
<HStack>
<Icon as={FiExternalLink} color="blue.500" />
<Text fontSize="xs" fontWeight="bold" color="gray.500" textTransform="uppercase">
Quick Admin Links
</Text>
</HStack>
<Box
p={3}
borderRadius="md"
bg={bgColor}
border="1px"
borderColor={borderColor}
>
<VStack align="stretch" spacing={2} divider={<Divider />}>
{links.map((link, index) => (
<Link
key={index}
href={link.url}
_hover={{ textDecoration: 'none' }}
isExternal
>
<HStack
p={2}
borderRadius="md"
transition="all 0.2s"
_hover={{
bg: useColorModeValue('white', 'gray.800'),
transform: 'translateX(4px)',
}}
justify="space-between"
>
<HStack spacing={3} flex={1}>
<Icon as={link.icon} boxSize={4} color="blue.500" />
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="medium">
{link.label}
</Text>
{link.description && (
<Text fontSize="xs" color="gray.500">
{link.description}
</Text>
)}
</VStack>
</HStack>
{link.badge && (
<Badge colorScheme="green" fontSize="xs">
{link.badge}
</Badge>
)}
<Icon as={FiExternalLink} boxSize={3} color="gray.400" />
</HStack>
</Link>
))}
</VStack>
</Box>
<Text fontSize="xs" color="gray.500" textAlign="center">
💡 These links help you manage content for this section
</Text>
</VStack>
</Box>
);
};
export default ContextualAdminLinks;
@@ -0,0 +1,297 @@
import React, { useState, useEffect } from 'react';
import {
Box,
VStack,
HStack,
Text,
Textarea,
Button,
IconButton,
useToast,
Tabs,
TabList,
Tab,
TabPanels,
TabPanel,
Code,
Alert,
AlertIcon,
useColorModeValue,
Divider,
} from '@chakra-ui/react';
import { FiCode, FiEye, FiSave, FiRefreshCw } from 'react-icons/fi';
interface CustomCSSEditorProps {
elementName: string;
onCSSChange: (css: string) => void;
currentCSS?: string;
}
const CustomCSSEditor: React.FC<CustomCSSEditorProps> = ({
elementName,
onCSSChange,
currentCSS = '',
}) => {
const [css, setCSS] = useState(currentCSS);
const [isValid, setIsValid] = useState(true);
const [preview, setPreview] = useState(false);
const toast = useToast();
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
useEffect(() => {
setCSS(currentCSS);
}, [currentCSS]);
const validateCSS = (cssString: string): boolean => {
try {
// Basic CSS validation
if (!cssString.trim()) return true;
// Check for balanced braces
const openBraces = (cssString.match(/{/g) || []).length;
const closeBraces = (cssString.match(/}/g) || []).length;
return openBraces === closeBraces;
} catch {
return false;
}
};
const handleCSSChange = (value: string) => {
setCSS(value);
const valid = validateCSS(value);
setIsValid(valid);
if (valid && preview) {
applyCSS(value);
}
};
const applyCSS = (cssString: string) => {
// Remove existing custom style
const existingStyle = document.getElementById(`custom-css-${elementName}`);
if (existingStyle) {
existingStyle.remove();
}
if (cssString.trim()) {
// Create new style element
const style = document.createElement('style');
style.id = `custom-css-${elementName}`;
style.textContent = `
[data-element="${elementName}"] {
${cssString}
}
`;
document.head.appendChild(style);
}
};
const handleSave = () => {
if (!isValid) {
toast({
title: 'Invalid CSS',
description: 'Please fix CSS errors before saving',
status: 'error',
duration: 3000,
});
return;
}
applyCSS(css);
onCSSChange(css);
toast({
title: 'CSS Applied',
description: 'Custom styles have been applied',
status: 'success',
duration: 2000,
});
};
const handleReset = () => {
setCSS('');
const existingStyle = document.getElementById(`custom-css-${elementName}`);
if (existingStyle) {
existingStyle.remove();
}
onCSSChange('');
};
const cssExamples = [
{
label: 'Background Gradient',
code: `background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;`,
},
{
label: 'Shadow & Hover',
code: `box-shadow: 0 10px 25px rgba(0,0,0,0.1);
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}`,
},
{
label: 'Border Radius',
code: `border-radius: 20px;
overflow: hidden;`,
},
{
label: 'Animation',
code: `animation: fadeIn 1s ease-in;
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}`,
},
];
return (
<Box width="100%" height="100%">
<Tabs size="sm" variant="enclosed" colorScheme="purple">
<TabList>
<Tab>
<HStack spacing={2}>
<FiCode />
<Text>CSS Editor</Text>
</HStack>
</Tab>
<Tab>
<HStack spacing={2}>
<FiEye />
<Text>Examples</Text>
</HStack>
</Tab>
</TabList>
<TabPanels>
{/* Editor Tab */}
<TabPanel p={0}>
<VStack align="stretch" spacing={3} p={4}>
<HStack justify="space-between">
<Text fontSize="xs" fontWeight="bold" color="gray.500" textTransform="uppercase">
Custom CSS for {elementName}
</Text>
<HStack spacing={2}>
<Button
size="xs"
leftIcon={<FiEye />}
variant={preview ? 'solid' : 'outline'}
colorScheme={preview ? 'blue' : 'gray'}
onClick={() => {
setPreview(!preview);
if (!preview && isValid) {
applyCSS(css);
}
}}
>
Preview
</Button>
<Button
size="xs"
leftIcon={<FiRefreshCw />}
variant="ghost"
onClick={handleReset}
>
Reset
</Button>
</HStack>
</HStack>
{!isValid && (
<Alert status="error" borderRadius="md" fontSize="sm">
<AlertIcon />
Invalid CSS syntax. Check for missing braces or semicolons.
</Alert>
)}
<Textarea
value={css}
onChange={(e) => handleCSSChange(e.target.value)}
placeholder={`/* Enter custom CSS properties */
background: #f0f0f0;
padding: 20px;
border-radius: 10px;`}
fontFamily="monospace"
fontSize="sm"
minHeight="300px"
bg={useColorModeValue('gray.50', 'gray.900')}
borderColor={isValid ? borderColor : 'red.300'}
_focus={{
borderColor: isValid ? 'purple.400' : 'red.400',
boxShadow: isValid ? '0 0 0 1px var(--chakra-colors-purple-400)' : '0 0 0 1px var(--chakra-colors-red-400)',
}}
/>
<Divider />
<Alert status="info" borderRadius="md" fontSize="xs">
<AlertIcon />
<Box>
<Text fontWeight="bold">Pro tip:</Text>
<Text>Use standard CSS properties. Avoid selectors - styles apply to the element automatically.</Text>
</Box>
</Alert>
<Button
leftIcon={<FiSave />}
colorScheme="purple"
size="sm"
onClick={handleSave}
isDisabled={!isValid}
>
Apply CSS
</Button>
</VStack>
</TabPanel>
{/* Examples Tab */}
<TabPanel>
<VStack align="stretch" spacing={3} p={4}>
<Text fontSize="xs" fontWeight="bold" color="gray.500" textTransform="uppercase">
Quick CSS Examples
</Text>
{cssExamples.map((example, index) => (
<Box
key={index}
p={3}
borderRadius="md"
border="1px"
borderColor={borderColor}
cursor="pointer"
transition="all 0.2s"
_hover={{
borderColor: 'purple.400',
transform: 'translateX(4px)',
}}
onClick={() => setCSS(example.code)}
>
<Text fontWeight="bold" fontSize="sm" mb={2}>
{example.label}
</Text>
<Code
fontSize="xs"
display="block"
whiteSpace="pre"
p={2}
borderRadius="sm"
bg={useColorModeValue('gray.100', 'gray.900')}
>
{example.code}
</Code>
</Box>
))}
</VStack>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
);
};
export default CustomCSSEditor;
@@ -0,0 +1,204 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Box,
IconButton,
HStack,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Button,
VStack,
Input,
useColorModeValue,
Tooltip,
} from '@chakra-ui/react';
import { FiBold, FiItalic, FiUnderline, FiType, FiLink, FiCheck, FiX } from 'react-icons/fi';
interface InlineTextEditorProps {
elementId: string;
onSave: (content: string) => void;
initialContent?: string;
}
const InlineTextEditor: React.FC<InlineTextEditorProps> = ({ elementId, onSave, initialContent = '' }) => {
const [isEditing, setIsEditing] = useState(false);
const [content, setContent] = useState(initialContent);
const [showLinkPopover, setShowLinkPopover] = useState(false);
const [linkUrl, setLinkUrl] = useState('');
const editorRef = useRef<HTMLDivElement>(null);
const bgColor = useColorModeValue('white', 'gray.800');
useEffect(() => {
if (isEditing && editorRef.current) {
editorRef.current.focus();
// Select all text
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(editorRef.current);
selection?.removeAllRanges();
selection?.addRange(range);
}
}, [isEditing]);
const handleFormat = (command: string, value?: string) => {
document.execCommand(command, false, value);
};
const handleSave = () => {
if (editorRef.current) {
const newContent = editorRef.current.innerHTML;
setContent(newContent);
onSave(newContent);
setIsEditing(false);
}
};
const handleCancel = () => {
if (editorRef.current) {
editorRef.current.innerHTML = content;
}
setIsEditing(false);
};
const handleInsertLink = () => {
if (linkUrl) {
handleFormat('createLink', linkUrl);
setLinkUrl('');
setShowLinkPopover(false);
}
};
return (
<Box position="relative">
{isEditing && (
<Box
position="absolute"
top="-50px"
left="0"
bg={bgColor}
borderRadius="md"
boxShadow="lg"
p={2}
zIndex={10000}
border="1px solid"
borderColor="blue.400"
>
<HStack spacing={1}>
<Tooltip label="Bold">
<IconButton
aria-label="Bold"
icon={<FiBold />}
size="sm"
variant="ghost"
onClick={() => handleFormat('bold')}
/>
</Tooltip>
<Tooltip label="Italic">
<IconButton
aria-label="Italic"
icon={<FiItalic />}
size="sm"
variant="ghost"
onClick={() => handleFormat('italic')}
/>
</Tooltip>
<Tooltip label="Underline">
<IconButton
aria-label="Underline"
icon={<FiUnderline />}
size="sm"
variant="ghost"
onClick={() => handleFormat('underline')}
/>
</Tooltip>
<Popover isOpen={showLinkPopover} onClose={() => setShowLinkPopover(false)}>
<PopoverTrigger>
<IconButton
aria-label="Link"
icon={<FiLink />}
size="sm"
variant="ghost"
onClick={() => setShowLinkPopover(true)}
/>
</PopoverTrigger>
<PopoverContent width="250px">
<PopoverBody>
<VStack spacing={2}>
<Input
placeholder="https://example.com"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
size="sm"
/>
<HStack width="100%">
<Button size="sm" colorScheme="blue" onClick={handleInsertLink} flex={1}>
Insert
</Button>
<Button size="sm" variant="ghost" onClick={() => setShowLinkPopover(false)}>
Cancel
</Button>
</HStack>
</VStack>
</PopoverBody>
</PopoverContent>
</Popover>
<Box width="1px" height="20px" bg="gray.300" mx={1} />
<Tooltip label="Save">
<IconButton
aria-label="Save"
icon={<FiCheck />}
size="sm"
colorScheme="green"
onClick={handleSave}
/>
</Tooltip>
<Tooltip label="Cancel">
<IconButton
aria-label="Cancel"
icon={<FiX />}
size="sm"
colorScheme="red"
variant="ghost"
onClick={handleCancel}
/>
</Tooltip>
</HStack>
</Box>
)}
<Box
ref={editorRef}
contentEditable={isEditing}
suppressContentEditableWarning
dangerouslySetInnerHTML={{ __html: content }}
onClick={() => !isEditing && setIsEditing(true)}
onBlur={(e) => {
// Don't close if clicking on toolbar
if (!e.relatedTarget || !(e.relatedTarget as HTMLElement).closest('[role="group"]')) {
// Auto-save on blur if changed
if (editorRef.current && editorRef.current.innerHTML !== content) {
handleSave();
}
}
}}
cursor={isEditing ? 'text' : 'pointer'}
outline={isEditing ? '2px solid' : 'none'}
outlineColor="blue.400"
outlineOffset="2px"
p={isEditing ? 2 : 0}
borderRadius="md"
transition="all 0.2s"
_hover={{
outline: isEditing ? '2px solid' : '2px dashed',
outlineColor: 'blue.400',
}}
/>
</Box>
);
};
export default InlineTextEditor;
+127 -12
View File
@@ -100,7 +100,7 @@ import {
import { useAuth } from '../../contexts/AuthContext';
import { useClubTheme } from '../../contexts/ClubThemeContext';
import VisualStylePanel from './VisualStylePanel';
import { DEFAULT_HOMEPAGE_ELEMENTS } from '../../data/defaultElements';
import { DEFAULT_HOMEPAGE_ELEMENTS, HOMEPAGE_IMPLEMENTED_ELEMENTS } from '../../data/defaultElements';
interface MyUIbrixStyleEditorProps {
pageType: string;
@@ -130,6 +130,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
const [visibleElements, setVisibleElements] = useState<Set<string>>(new Set());
const [elementOrder, setElementOrder] = useState<string[]>([]);
const [draggedElement, setDraggedElement] = useState<string | null>(null);
const [dragOverElement, setDragOverElement] = useState<string | null>(null);
const [viewport, setViewport] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
const [elementStyles, setElementStyles] = useState<Record<string, any>>({});
const [showStylePanel, setShowStylePanel] = useState(true);
@@ -360,7 +361,13 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
});
};
Object.keys(ELEMENT_VARIANTS).forEach(addOverlay);
// Only add overlays for elements that are actually implemented on this page
const implementedElements = pageType === 'homepage' ? HOMEPAGE_IMPLEMENTED_ELEMENTS : Object.keys(ELEMENT_VARIANTS);
implementedElements.forEach((elementName) => {
if (ELEMENT_VARIANTS[elementName]) {
addOverlay(elementName);
}
});
// Close panel on escape
const handleEscape = (e: KeyboardEvent) => {
@@ -374,7 +381,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
document.querySelectorAll('.elementor-overlay').forEach(el => el.remove());
document.removeEventListener('keydown', handleEscape);
};
}, [isEditing, selectedElement]);
}, [isEditing, selectedElement, pageType]);
// Update selected element overlay styling
useEffect(() => {
@@ -481,8 +488,9 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
setElementOrder(newOrder);
setHasChanges(true);
// Trigger reorder event ONLY during editing
// Trigger reorder event and apply visual reordering
if (isEditing) {
applyVisualReorder(newOrder);
window.dispatchEvent(new CustomEvent('myuibrix-reorder', {
detail: { order: newOrder, previewMode: true }
}));
@@ -498,14 +506,92 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
setElementOrder(newOrder);
setHasChanges(true);
// Trigger reorder event ONLY during editing
// Trigger reorder event and apply visual reordering
if (isEditing) {
applyVisualReorder(newOrder);
window.dispatchEvent(new CustomEvent('myuibrix-reorder', {
detail: { order: newOrder, previewMode: true }
}));
}
}, [elementOrder, isEditing]);
// Apply visual reordering to DOM elements
const applyVisualReorder = useCallback((order: string[]) => {
const container = document.querySelector('.container');
if (!container) return;
// Get all sections with data-element attributes
const sections = Array.from(container.querySelectorAll('[data-element]')) as HTMLElement[];
// Create a map of element names to their DOM nodes
const elementMap = new Map<string, HTMLElement>();
sections.forEach(section => {
const elementName = section.getAttribute('data-element');
if (elementName) {
elementMap.set(elementName, section);
}
});
// Reorder by appending in the correct order
order.forEach((elementName) => {
const element = elementMap.get(elementName);
if (element && element.parentElement === container) {
container.appendChild(element);
}
});
}, []);
// Drag and drop handlers
const handleDragStart = useCallback((elementName: string) => {
setDraggedElement(elementName);
}, []);
const handleDragOver = useCallback((e: React.DragEvent, elementName: string) => {
e.preventDefault();
setDragOverElement(elementName);
}, []);
const handleDragLeave = useCallback(() => {
setDragOverElement(null);
}, []);
const handleDrop = useCallback((e: React.DragEvent, targetElementName: string) => {
e.preventDefault();
if (!draggedElement || draggedElement === targetElementName) {
setDraggedElement(null);
setDragOverElement(null);
return;
}
const newOrder = [...elementOrder];
const draggedIndex = newOrder.indexOf(draggedElement);
const targetIndex = newOrder.indexOf(targetElementName);
if (draggedIndex === -1 || targetIndex === -1) {
setDraggedElement(null);
setDragOverElement(null);
return;
}
// Remove dragged element and insert at target position
newOrder.splice(draggedIndex, 1);
newOrder.splice(targetIndex, 0, draggedElement);
setElementOrder(newOrder);
setHasChanges(true);
setDraggedElement(null);
setDragOverElement(null);
// Apply visual reordering
if (isEditing) {
applyVisualReorder(newOrder);
window.dispatchEvent(new CustomEvent('myuibrix-reorder', {
detail: { order: newOrder, previewMode: true }
}));
}
}, [draggedElement, elementOrder, isEditing, applyVisualReorder]);
const handleSave = async () => {
try {
const configsToSave: PageElementConfig[] = elementOrder.map((elementName, index) => ({
@@ -1125,21 +1211,36 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
const isVisible = visibleElements.has(elementName);
const isSelected = selectedElement === elementName;
const isDragging = draggedElement === elementName;
const isDragOver = dragOverElement === elementName;
return (
<Box
key={elementName}
p={3}
borderRadius="lg"
border="2px"
borderColor={isSelected ? secondaryColor : borderColor}
bg={isSelected ? `${secondaryColor}20` : isVisible ? bgColor : 'gray.100'}
cursor="pointer"
opacity={isVisible ? 1 : 0.5}
borderColor={isDragOver ? 'blue.500' : isSelected ? secondaryColor : borderColor}
bg={isDragging ? 'gray.200' : isSelected ? `${secondaryColor}20` : isVisible ? bgColor : 'gray.100'}
cursor={isDragging ? 'grabbing' : 'grab'}
opacity={isDragging ? 0.5 : isVisible ? 1 : 0.5}
transition="all 0.2s"
transform={isDragOver ? 'scale(1.05)' : undefined}
_hover={{
borderColor: secondaryColor,
transform: 'translateX(4px)',
transform: isDragOver ? 'scale(1.05)' : 'translateX(4px)',
}}
draggable
onDragStart={(e) => {
handleDragStart(elementName);
(e.target as HTMLElement).style.cursor = 'grabbing';
}}
onDragEnd={(e) => {
(e.target as HTMLElement).style.cursor = 'grab';
}}
onDragOver={(e) => handleDragOver(e, elementName)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, elementName)}
onClick={() => {
setSelectedElement(elementName);
const el = document.querySelector(`[data-element="${elementName}"]`);
@@ -1155,7 +1256,14 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
}}
>
<Flex align="center" justify="space-between">
<HStack flex={1}>
<HStack flex={1} spacing={2}>
<Icon
as={FaGripVertical}
boxSize={4}
color="gray.400"
cursor="grab"
_active={{ cursor: 'grabbing' }}
/>
<Icon as={element?.icon || FaCube} boxSize={5} color={secondaryColor} />
<VStack align="start" spacing={0} flex={1}>
<Text fontWeight="bold" fontSize="sm">
@@ -1339,7 +1447,14 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
// Filter by category selection
if (selectedCategory !== 'all' && selectedCategory !== category) return null;
const elements = PREDEFINED_ELEMENTS.filter(e => e.category === category);
// IMPORTANT: Only show elements that are actually implemented on this page
const implementedForThisPage = pageType === 'homepage' ? HOMEPAGE_IMPLEMENTED_ELEMENTS : [];
const elements = PREDEFINED_ELEMENTS.filter(e =>
e.category === category &&
(implementedForThisPage.length === 0 || implementedForThisPage.includes(e.name))
);
const availableElements = elements.filter(e => {
if (visibleElements.has(e.name)) return false;
// Filter by search query
@@ -28,8 +28,11 @@ import {
Button,
useColorModeValue,
} from '@chakra-ui/react';
import { FiType, FiLayout, FiBox, FiDroplet, FiGrid, FiSmartphone, FiBarChart2, FiSidebar } from 'react-icons/fi';
import { FiType, FiLayout, FiBox, FiDroplet, FiGrid, FiSmartphone, FiBarChart2, FiSidebar, FiCode, FiColumns, FiExternalLink } from 'react-icons/fi';
import { FaRegNewspaper, FaRegSquare, FaColumns } from 'react-icons/fa';
import CustomCSSEditor from './CustomCSSEditor';
import ColumnLayoutManager from './ColumnLayoutManager';
import ContextualAdminLinks from './ContextualAdminLinks';
import { useClubTheme } from '../../contexts/ClubThemeContext';
interface VisualStylePanelProps {
@@ -85,6 +88,9 @@ const VisualStylePanel: React.FC<VisualStylePanelProps> = ({
alignItems: currentStyles.alignItems || 'stretch',
justifyItems: currentStyles.justifyItems || 'stretch',
// Custom CSS
customCSS: currentStyles.customCSS || '',
...currentStyles,
});
@@ -105,11 +111,12 @@ const VisualStylePanel: React.FC<VisualStylePanelProps> = ({
pt="60px"
>
<Tabs size="sm" colorScheme="blue">
<TabList px={2}>
<TabList px={2} flexWrap="wrap">
<Tab><FiType /> <Text ml={1}>Content</Text></Tab>
<Tab><FiLayout /> <Text ml={1}>Style</Text></Tab>
<Tab><FiGrid /> <Text ml={1}>Grid</Text></Tab>
<Tab><FiBox /> <Text ml={1}>Advanced</Text></Tab>
<Tab><FiColumns /> <Text ml={1}>Layout</Text></Tab>
<Tab><FiCode /> <Text ml={1}>CSS</Text></Tab>
<Tab><FiExternalLink /> <Text ml={1}>Admin</Text></Tab>
</TabList>
<TabPanels>
@@ -403,7 +410,7 @@ const VisualStylePanel: React.FC<VisualStylePanelProps> = ({
</VStack>
</TabPanel>
{/* Grid Tab */}
{/* Layout Tab (was Grid Tab) */}
<TabPanel>
<VStack align="stretch" spacing={4}>
<Text fontWeight="bold" fontSize="sm" textTransform="uppercase" color="gray.500">
@@ -658,51 +665,18 @@ const VisualStylePanel: React.FC<VisualStylePanelProps> = ({
</VStack>
</TabPanel>
{/* Advanced Tab */}
{/* Custom CSS Tab */}
<TabPanel p={0}>
<CustomCSSEditor
elementName={elementName}
onCSSChange={(css) => updateStyle('customCSS', css)}
currentCSS={styles.customCSS || ''}
/>
</TabPanel>
{/* Admin Links Tab */}
<TabPanel>
<VStack align="stretch" spacing={4}>
<Text fontWeight="bold" fontSize="sm" textTransform="uppercase" color="gray.500">
Layout
</Text>
{/* Display */}
<FormControl>
<FormLabel fontSize="xs">Display</FormLabel>
<Select
size="sm"
value={styles.display}
onChange={(e) => updateStyle('display', e.target.value)}
>
<option value="block">Block</option>
<option value="inline-block">Inline Block</option>
<option value="flex">Flex</option>
<option value="grid">Grid</option>
<option value="none">None</option>
</Select>
</FormControl>
{/* Width */}
<FormControl>
<FormLabel fontSize="xs">Width</FormLabel>
<Input
size="sm"
value={styles.width}
onChange={(e) => updateStyle('width', e.target.value)}
placeholder="auto, 100%, 500px"
/>
</FormControl>
{/* Height */}
<FormControl>
<FormLabel fontSize="xs">Height</FormLabel>
<Input
size="sm"
value={styles.height}
onChange={(e) => updateStyle('height', e.target.value)}
placeholder="auto, 100%, 500px"
/>
</FormControl>
</VStack>
<ContextualAdminLinks elementName={elementName} />
</TabPanel>
</TabPanels>
</Tabs>
@@ -6,6 +6,7 @@ import { usePublicSettings } from '../../hooks/usePublicSettings';
import { getCompetitionAliasesPublic, CompetitionAlias } from '../../services/competitionAliases';
import { TeamLogo } from '../common/TeamLogo';
import { sortCategoriesWithOrder } from '../../utils/categorySort';
import { sanitizeClubName } from '../../utils/url';
import '../../styles/logos.css';
const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string; aid?: string; al?: string; s?: string; clubName?: string }> = ({ d, h, hid, hl, a, aid, al, s, clubName }) => (
@@ -13,7 +14,7 @@ const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string
<Text w="140px" fontSize="sm" color="gray.600">{d}</Text>
<HStack flex={1} justify="flex-end" spacing={4}>
<HStack minW="40%" justify="flex-end" spacing={2}>
<Text noOfLines={1} textAlign="right" flex={1}>{h}</Text>
<Text noOfLines={1} textAlign="right" flex={1}>{sanitizeClubName(h)}</Text>
<Box className="logo-container" w="28px" h="28px">
<TeamLogo
teamId={hid}
@@ -51,7 +52,7 @@ const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string
boxSize="28px"
/>
</Box>
<Text noOfLines={1} flex={1}>{a}</Text>
<Text noOfLines={1} flex={1}>{sanitizeClubName(a)}</Text>
</HStack>
</HStack>
</HStack>
+4 -4
View File
@@ -17,7 +17,7 @@ import {
Divider,
} from '@chakra-ui/react';
import { useCountdown } from '../../hooks/useCountdown';
import { assetUrl } from '../../utils/url';
import { assetUrl, sanitizeClubName } from '../../utils/url';
export type FacrMatchLike = {
id?: string | number;
@@ -93,7 +93,7 @@ export const MatchModal: React.FC<MatchModalProps> = ({ isOpen, match, onClose,
<ModalOverlay />
<ModalContent>
<ModalHeader>
{match?.home || 'Domácí'} vs {match?.away || 'Hosté'}
{sanitizeClubName(match?.home) || 'Domácí'} vs {sanitizeClubName(match?.away) || 'Hosté'}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
@@ -113,7 +113,7 @@ export const MatchModal: React.FC<MatchModalProps> = ({ isOpen, match, onClose,
tabIndex={onTeamClick ? 0 : undefined}
>
<Image src={assetUrl(match.home_logo_url) || '/logo192.png'} alt={match.home || 'Domácí'} boxSize="56px" objectFit="contain" />
<Text fontWeight="semibold" noOfLines={1} textAlign="center">{match.home || 'Domácí'}</Text>
<Text fontWeight="semibold" noOfLines={1} textAlign="center">{sanitizeClubName(match.home) || 'Domácí'}</Text>
</VStack>
<VStack spacing={1} minW="120px">
{hasScore ? (
@@ -149,7 +149,7 @@ export const MatchModal: React.FC<MatchModalProps> = ({ isOpen, match, onClose,
tabIndex={onTeamClick ? 0 : undefined}
>
<Image src={assetUrl(match.away_logo_url) || '/logo192.png'} alt={match.away || 'Hosté'} boxSize="56px" objectFit="contain" />
<Text fontWeight="semibold" noOfLines={1} textAlign="center">{match.away || 'Hosté'}</Text>
<Text fontWeight="semibold" noOfLines={1} textAlign="center">{sanitizeClubName(match.away) || 'Hosté'}</Text>
</VStack>
</HStack>
@@ -17,7 +17,7 @@ const MatchRow: React.FC<{
<Text w="140px" fontSize="sm" color="gray.600">{date}</Text>
<HStack flex={1} justify="flex-end">
<HStack minW="40%" justify="flex-end" spacing={2}>
<Text noOfLines={1} textAlign="right" flex={1}>{home.name}</Text>
<Text noOfLines={1} textAlign="right" flex={1} fontSize="sm" lineHeight="1.2em" height="1.2em" overflow="hidden">{home.name}</Text>
<Box className="logo-container" w="28px" h="28px">
<TeamLogo
teamId={home.id}
@@ -58,7 +58,7 @@ const MatchRow: React.FC<{
boxSize="28px"
/>
</Box>
<Text noOfLines={1} flex={1}>{away.name}</Text>
<Text noOfLines={1} flex={1} fontSize="sm" lineHeight="1.2em" height="1.2em" overflow="hidden">{away.name}</Text>
</HStack>
</HStack>
</HStack>
@@ -180,7 +180,6 @@ const VideosSection: React.FC<Props> = ({ videos }) => {
display="flex"
alignItems="center"
justifyContent="center"
bg="blackAlpha.700"
opacity={0}
transition="opacity 0.3s ease"
pointerEvents="none"
@@ -10,7 +10,7 @@ import { format, parse, isToday, isTomorrow, isAfter } from 'date-fns';
import { cs } from 'date-fns/locale';
import { Match } from '../../types';
import { fetchTeamLogoOverrides } from '@/services/adminMatches';
import { assetUrl } from '@/utils/url';
import { assetUrl, sanitizeClubName } from '@/utils/url';
import { TeamLogo } from '../common/TeamLogo';
import '../../styles/logos.css';
@@ -247,7 +247,7 @@ export const MatchesWidget = () => {
isTruncated
color="gray.800"
>
{match.home}
{sanitizeClubName(match.home)}
</Text>
</HStack>
<Text
@@ -268,7 +268,7 @@ export const MatchesWidget = () => {
textAlign="right"
color="gray.800"
>
{match.away}
{sanitizeClubName(match.away)}
</Text>
<Box flexShrink={0} className="match-widget-logo">
<TeamLogo