mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #62
This commit is contained in:
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user