dev day #65,5

This commit is contained in:
Tomas Dvorak
2025-10-20 10:40:55 +02:00
parent 9ccca365b3
commit 68e69e00cc
41 changed files with 981 additions and 1376 deletions
@@ -274,11 +274,30 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
// Insert into editor
const quill = quillRef.current?.getEditor();
if (quill) {
const range = quill.getSelection(true);
const index = range ? range.index : quill.getLength();
quill.insertEmbed(index, 'image', res.url, 'user');
quill.setSelection(index + 1, 0);
toast({ title: 'Obrázek vložen', status: 'success', duration: 2000 });
// Ensure editor is focused and ready
quill.focus();
// Use setTimeout to ensure Quill's internal state is ready
setTimeout(() => {
try {
const range = quill.getSelection();
const index = range ? range.index : quill.getLength();
// Insert the image
quill.insertEmbed(index, 'image', res.url, 'api');
// Move cursor after the image
quill.setSelection(index + 1, 0, 'api');
// Force content change to trigger re-render
onChangeRef.current(quill.root.innerHTML);
toast({ title: 'Obrázek vložen', status: 'success', duration: 2000 });
} catch (embedError) {
console.error('Error inserting image:', embedError);
toast({ title: 'Chyba při vkládání obrázku', description: String(embedError), status: 'error' });
}
}, 50);
}
} catch (e: any) {
console.error('Crop and insert error:', e);
@@ -500,6 +519,10 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
img.style.outline = '3px solid #3182ce';
img.style.cursor = 'move';
img.style.boxShadow = '0 4px 12px rgba(49, 130, 206, 0.3)';
// Prevent default drag behavior to avoid duplication
img.setAttribute('draggable', 'false');
createResizeHandle(img);
// Set selected image state and load filters
@@ -639,7 +662,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
currentAlignment = 'right';
}
// Disable default drag behavior to prevent ghost image
// Already set in selectImage, but ensure it's off
target.setAttribute('draggable', 'false');
const onMouseMove = (e: MouseEvent) => {
@@ -709,15 +732,27 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
}
};
// Prevent default drag behavior on images
const handleDragStart = (e: DragEvent) => {
const target = e.target as HTMLElement;
if (target.tagName === 'IMG') {
e.preventDefault();
e.stopPropagation();
return false;
}
};
editor.root.addEventListener('click', handleImageClick);
editor.root.addEventListener('mousedown', handleMouseDown);
editor.root.addEventListener('scroll', handleScroll);
editor.root.addEventListener('dragstart', handleDragStart);
document.addEventListener('keydown', handleKeyDown);
return () => {
editor.root.removeEventListener('click', handleImageClick);
editor.root.removeEventListener('mousedown', handleMouseDown);
editor.root.removeEventListener('scroll', handleScroll);
editor.root.removeEventListener('dragstart', handleDragStart);
document.removeEventListener('keydown', handleKeyDown);
removeResizeHandle();
deselectImage();
@@ -1107,6 +1142,9 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
transition: 'all 0.2s ease',
borderRadius: '4px',
userSelect: 'none',
pointerEvents: 'auto',
WebkitUserDrag: 'none',
userDrag: 'none',
'&:hover': {
opacity: 0.95,
transform: 'scale(1.01)',
+63 -7
View File
@@ -122,14 +122,70 @@ const FilePreview: React.FC<FilePreviewProps> = ({
}
if (fileInfo.type === 'pdf') {
// Try multiple PDF viewing methods due to CSP restrictions
return (
<AspectRatio ratio={8.5 / 11} w="100%" minH="70vh">
<iframe
src={`${fullUrl}#view=FitH`}
title={fileName}
style={{ border: 'none', width: '100%', height: '100%' }}
/>
</AspectRatio>
<VStack spacing={4} w="100%" minH="70vh">
{/* Primary: Try direct iframe embed */}
<Box w="100%" h="70vh" borderWidth="1px" borderRadius="md" overflow="hidden">
<iframe
src={`${fullUrl}#view=FitH&toolbar=1`}
title={fileName}
style={{ border: 'none', width: '100%', height: '100%' }}
onError={(e) => {
console.error('PDF iframe load error:', e);
}}
/>
</Box>
{/* Fallback options */}
<VStack spacing={2} w="100%">
<Text fontSize="sm" color={mutedText} textAlign="center">
Pokud se PDF nezobrazuje, použijte jedno z tlačítek níže:
</Text>
<HStack spacing={3} flexWrap="wrap" justify="center">
<Button
as={ChakraLink}
href={fullUrl}
isExternal
leftIcon={<FiEye />}
colorScheme="blue"
size="sm"
>
Otevřít v novém okně
</Button>
<Button
as={ChakraLink}
href={`https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent(fullUrl)}`}
isExternal
leftIcon={<FiEye />}
colorScheme="purple"
size="sm"
>
Zobrazit pomocí PDF.js
</Button>
<Button
as={ChakraLink}
href={`https://docs.google.com/viewer?url=${encodeURIComponent(fullUrl)}&embedded=true`}
isExternal
leftIcon={<FiEye />}
colorScheme="green"
size="sm"
>
Zobrazit přes Google
</Button>
<Button
as={ChakraLink}
href={fullUrl}
download
leftIcon={<FiDownload />}
colorScheme="gray"
size="sm"
>
Stáhnout PDF
</Button>
</HStack>
</VStack>
</VStack>
);
}
@@ -9,7 +9,7 @@ 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 }) => (
const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string; aid?: string; al?: string; s?: string; clubName?: string; clubId?: string }> = ({ d, h, hid, hl, a, aid, al, s, clubName, clubId }) => (
<HStack justify="space-between" borderRadius="lg" p={3} bg="white" boxShadow="sm">
<Text w="140px" fontSize="sm" color="gray.600">{d}</Text>
<HStack flex={1} justify="flex-end" spacing={4}>
@@ -28,14 +28,30 @@ const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string
<HStack minW="60px" justify="center" spacing={2}>
<Text fontWeight="bold" textAlign="center">{s || '-:-'}</Text>
{(() => {
if (!s || !clubName) return null;
if (!s) return null;
const m = s.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
if (!m) return null;
const hG = parseInt(m[1], 10), aG = parseInt(m[2], 10);
const norm = (x: string) => String(x||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
const strip = (x: string) => norm(x).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
const ourHome = (() => { const A = strip(h); const B = strip(clubName); return A && B && (A===B || A.endsWith(B) || B.endsWith(A)); })();
const ourAway = (() => { const A = strip(a); const B = strip(clubName); return A && B && (A===B || A.endsWith(B) || B.endsWith(A)); })();
// First try ID-based matching (most reliable)
let ourHome = false;
let ourAway = false;
if (clubId && hid && aid) {
ourHome = hid === clubId;
ourAway = aid === clubId;
}
// Fallback to name matching if IDs not available or no match
if (!ourHome && !ourAway && clubName) {
const norm = (x: string) => String(x||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
const strip = (x: string) => norm(x).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
const A = strip(h);
const B = strip(clubName);
ourHome = Boolean(A && B && (A===B || A.endsWith(B) || B.endsWith(A)));
const C = strip(a);
ourAway = Boolean(C && B && (C===B || C.endsWith(B) || B.endsWith(C)));
}
if (!ourHome && !ourAway) return null;
if (hG === aG) return <Badge colorScheme="blue" variant="subtle">Remíza</Badge>;
const our = ourHome ? hG : aG; const opp = ourHome ? aG : hG;
@@ -135,7 +151,7 @@ const CompetitionMatches: React.FC = () => {
<TabPanel key={c.id} px={0}>
<VStack align="stretch" spacing={3}>
{(c.matches || []).slice(0, 6).map((m, idx) => (
<Row key={m.match_id || idx} d={m.date_time} h={m.home} hid={m.home_id} hl={m.home_logo_url} a={m.away} aid={m.away_id} al={m.away_logo_url} s={m.score} clubName={data.name} />
<Row key={m.match_id || idx} d={m.date_time} h={m.home} hid={m.home_id} hl={m.home_logo_url} a={m.away} aid={m.away_id} al={m.away_logo_url} s={m.score} clubName={data.name} clubId={data.club_internal_id} />
))}
{(c.matches || []).length === 0 && (
<Text color="gray.500">Žádné zápasy k dispozici.</Text>
@@ -12,7 +12,8 @@ const MatchRow: React.FC<{
away: { name: string; logo?: string; id?: string };
score?: string;
clubName?: string;
}> = ({ date, home, away, score, clubName }) => (
clubId?: string;
}> = ({ date, home, away, score, clubName, clubId }) => (
<HStack justify="space-between" borderWidth="1px" borderRadius="md" p={3} bg="white">
<Text w="140px" fontSize="sm" color="gray.600">{date}</Text>
<HStack flex={1} justify="flex-end">
@@ -32,14 +33,30 @@ const MatchRow: React.FC<{
<Text fontWeight="bold" textAlign="center">{score || '-:-'}</Text>
{(() => {
const sent = (() => {
if (!score || !clubName) return null;
if (!score) return null;
const m = score.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
if (!m) return null;
const h = parseInt(m[1], 10), a = parseInt(m[2], 10);
const norm = (s: string) => String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g,' ').trim().toLowerCase();
const strip = (s: string) => norm(s).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
const ourIsHome = (() => { const aName = strip(home.name); const bName = strip(clubName); return aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)); })();
const ourIsAway = (() => { const aName = strip(away.name); const bName = strip(clubName); return aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)); })();
// First try ID-based matching (most reliable)
let ourIsHome = false;
let ourIsAway = false;
if (clubId && home.id && away.id) {
ourIsHome = home.id === clubId;
ourIsAway = away.id === clubId;
}
// Fallback to name matching if IDs not available or no match
if (!ourIsHome && !ourIsAway && clubName) {
const norm = (s: string) => String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g,' ').trim().toLowerCase();
const strip = (s: string) => norm(s).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
const aName = strip(home.name);
const bName = strip(clubName);
ourIsHome = Boolean(aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)));
const cName = strip(away.name);
ourIsAway = Boolean(cName && bName && (cName===bName || cName.endsWith(bName) || bName.endsWith(cName)));
}
if (!ourIsHome && !ourIsAway) return null;
if (h === a) return { label: 'Remíza', color: 'blue' } as const;
const our = ourIsHome ? h : a; const opp = ourIsHome ? a : h;
@@ -100,6 +117,7 @@ const MatchesSection: React.FC = () => {
away={{ name: m.away, logo: m.away_logo_url, id: m.away_id }}
score={m.score}
clubName={data.name}
clubId={data.club_internal_id}
/>
))}
{(c.matches || []).length === 0 && (
+23 -2
View File
@@ -22,6 +22,8 @@ type MatchItem = {
time: string; // HH:MM
home: string;
away: string;
home_id?: string;
away_id?: string;
venue?: string;
home_logo_url?: string;
away_logo_url?: string;
@@ -51,6 +53,7 @@ const CalendarPage: React.FC = () => {
const [searchParams] = useSearchParams();
const toast = useToast();
const [clubName, setClubName] = useState<string>('');
const [clubId, setClubId] = useState<string>('');
const [clubType, setClubType] = useState<'football' | 'futsal'>('football');
const [standings, setStandings] = useState<any[]>([]);
@@ -264,6 +267,8 @@ const CalendarPage: React.FC = () => {
time,
home: m.home,
away: m.away,
home_id: m.home_id,
away_id: m.away_id,
venue: m.venue,
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
@@ -307,6 +312,8 @@ const CalendarPage: React.FC = () => {
time,
home: m.home,
away: m.away,
home_id: m.home_id,
away_id: m.away_id,
venue: m.venue,
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
@@ -399,6 +406,7 @@ const CalendarPage: React.FC = () => {
setCompLinks(compLinkMap);
setStandings(standingsData);
if (json?.name) setClubName(String(json.name));
if (json?.club_internal_id) setClubId(String(json.club_internal_id));
if (json?.club_type) setClubType(json.club_type);
// Set active tab from query ?comp=<id>
const compQ = searchParams.get('comp');
@@ -521,8 +529,21 @@ const CalendarPage: React.FC = () => {
const s = parseScore(m.score);
if (!s) return null;
const ourIsHome = isClubTeam(m.home);
const ourIsAway = isClubTeam(m.away);
// First try ID-based matching (most reliable)
let ourIsHome = false;
let ourIsAway = false;
if (clubId && m.home_id && m.away_id) {
ourIsHome = m.home_id === clubId;
ourIsAway = m.away_id === clubId;
}
// Fallback to name matching if IDs not available or no match
if (!ourIsHome && !ourIsAway) {
ourIsHome = isClubTeam(m.home);
ourIsAway = isClubTeam(m.away);
}
if (!ourIsHome && !ourIsAway) return null; // unknown perspective
if (s.h === s.a) return { label: 'Remíza', color: 'blue' };
const ourGoals = ourIsHome ? s.h : s.a;
+20 -3
View File
@@ -15,6 +15,8 @@ type MatchItem = {
time: string;
home: string;
away: string;
home_id?: string;
away_id?: string;
home_logo_url?: string;
away_logo_url?: string;
score?: string | null;
@@ -23,7 +25,7 @@ type MatchItem = {
venue?: string;
competition?: string;
competitionName?: string;
};
}
const MatchesPage: React.FC = () => {
const [clubName, setClubName] = useState<string>('');
@@ -115,8 +117,21 @@ const MatchesPage: React.FC = () => {
const getSentiment = (m: MatchItem): { label: 'Výhra'|'Remíza'|'Prohra'; colorScheme: 'green'|'blue'|'red' } | null => {
const s = parseScore(m.score);
if (!s) return null;
const ourIsHome = isClubTeam(m.home);
const ourIsAway = isClubTeam(m.away);
// First try ID-based matching (most reliable)
let ourIsHome = false;
let ourIsAway = false;
if (clubId && m.home_id && m.away_id) {
ourIsHome = m.home_id === clubId;
ourIsAway = m.away_id === clubId;
}
// Fallback to name matching if IDs not available or no match
if (!ourIsHome && !ourIsAway) {
ourIsHome = isClubTeam(m.home);
ourIsAway = isClubTeam(m.away);
}
if (!ourIsHome && !ourIsAway) return null;
if (s.h === s.a) return { label: 'Remíza', colorScheme: 'blue' };
const ourGoals = ourIsHome ? s.h : s.a;
@@ -343,6 +358,8 @@ const MatchesPage: React.FC = () => {
time,
home: m.home,
away: m.away,
home_id: m.home_id,
away_id: m.away_id,
home_logo_url: getOverrideLogo(m.home, m.home_id, m.home_logo_url),
away_logo_url: getOverrideLogo(m.away, m.away_id, m.away_logo_url),
score: actualScore,
+121 -46
View File
@@ -36,6 +36,12 @@ const MatchLinkBadge: React.FC<{ articleId: number }> = ({ articleId }) => {
staleTime: 60_000,
retry: false,
});
// Show loading state while fetching
if (linkQ.isLoading) {
return <Badge colorScheme="gray">Načítání...</Badge>;
}
const mid = (linkQ.data as any)?.external_match_id;
if (!mid) return <Badge colorScheme="gray">Nepropojeno</Badge>;
@@ -651,38 +657,19 @@ const ArticlesAdminPage = () => {
// Forward the payload as-is so new fields (youtube, gallery) are persisted
createArticle(payload),
onSuccess: async (created: any) => {
try {
// If a match was selected (from temp storage), link it now that we have an article ID
const matchRaw = tempMatchLink || matchIdInput;
const matchToLink = typeof matchRaw === 'string' ? matchRaw : String(matchRaw || '');
const matchId = matchToLink.trim();
if (matchId && created?.id) {
await putArticleMatchLink(created.id, { external_match_id: matchId, title: (editing as any)?.title || '' });
setLinkedMatchId(matchId);
toast({
title: 'Článek vytvořen a propojen se zápasem',
description: `Match ID: ${matchId}`,
status: 'success',
duration: 3000,
isClosable: true
});
} else {
toast({
title: 'Článek byl úspěšně vytvořen',
status: 'success',
duration: 3000,
isClosable: true
});
}
// Clear temporary storage after successful creation
setTempMatchLink('');
} finally {
qc.invalidateQueries({ queryKey: ['admin-articles'] });
qc.invalidateQueries({ queryKey: ['articles'] });
qc.invalidateQueries({ queryKey: ['recentArticles'] });
qc.invalidateQueries({ queryKey: ['article-match-link'] }); // Invalidate match links
closeModal();
}
console.log('Article created successfully in mutation callback:', created);
// Note: Match linking is now handled in onSubmit() to avoid race conditions
// Clear temporary storage
setTempMatchLink('');
setMatchIdInput('');
// Invalidate queries to refresh the list
qc.invalidateQueries({ queryKey: ['admin-articles'] });
qc.invalidateQueries({ queryKey: ['articles'] });
qc.invalidateQueries({ queryKey: ['recentArticles'] });
qc.invalidateQueries({ queryKey: ['article-match-link'] }); // Invalidate match links
// Don't close modal here - let onSubmit handle it after match linking
},
onError: (e: any) => {
console.error('Error creating article:', e);
@@ -702,18 +689,16 @@ const ArticlesAdminPage = () => {
updateArticle(id, payload),
onSuccess: (_, variables) => {
const articleId = variables.id;
toast({
title: 'Článek byl úspěšně aktualizován',
status: 'success',
duration: 3000,
isClosable: true
});
console.log('Article updated successfully in mutation callback:', articleId);
// Invalidate queries to refresh the list
qc.invalidateQueries({ queryKey: ['admin-articles'] });
qc.invalidateQueries({ queryKey: ['articles'] });
qc.invalidateQueries({ queryKey: ['recentArticles'] });
qc.invalidateQueries({ queryKey: ['article-match-link', articleId] }); // Invalidate specific match link
qc.invalidateQueries({ queryKey: ['article', `id:${articleId}`] }); // Invalidate article detail
closeModal();
// Success toast and modal closing handled in onSubmit()
},
onError: (e: any) => {
console.error('Error updating article:', e);
@@ -864,7 +849,7 @@ const ArticlesAdminPage = () => {
} catch { return undefined; }
};
const onSubmit = async () => {
const onSubmit = async (options: { keepOpen?: boolean } = {}) => {
if (!editing) return;
// Require category selection by name (kategorie je povinná)
if (!String((editing as any)?.category_name || '').trim()) {
@@ -932,10 +917,83 @@ const ArticlesAdminPage = () => {
// Log the payload for debugging
console.log('Saving article with payload:', JSON.stringify(payload, null, 2));
// Debug: Log match link state before submission
console.log('Match link state before submit:', {
tempMatchLink,
matchIdInput,
linkedMatchId,
isNewArticle: !(editing as any)?.id
});
if ((editing as any)?.id) {
// Update existing article
await updateMut.mutateAsync({ id: (editing as any).id, payload });
// Handle match linking for existing articles (update or delete)
const matchRaw = matchIdInput || linkedMatchId;
const matchId = String(matchRaw || '').trim();
let matchLinked = false;
if (matchId) {
try {
await putArticleMatchLink((editing as any).id, { external_match_id: matchId, title: editing.title || '' });
console.log('Match link updated for existing article');
matchLinked = true;
} catch (err: any) {
console.error('Failed to update match link:', err);
}
}
// Show success message
toast({
title: matchLinked ? 'Článek aktualizován a propojen se zápasem' : 'Článek byl úspěšně aktualizován',
status: 'success',
duration: 3000,
isClosable: true
});
} else {
await createMut.mutateAsync(payload);
// Create new article
const created = await createMut.mutateAsync(payload);
// Handle match linking for new articles
const matchRaw = tempMatchLink || matchIdInput;
const matchId = String(matchRaw || '').trim();
if (matchId && created?.id) {
console.log('Linking new article', created.id, 'with match', matchId);
try {
await putArticleMatchLink(created.id, { external_match_id: matchId, title: editing.title || '' });
console.log('Match link created for new article');
setLinkedMatchId(matchId);
toast({
title: 'Článek vytvořen a propojen se zápasem',
description: `Match ID: ${matchId}`,
status: 'success',
duration: 3000,
isClosable: true
});
} catch (err: any) {
console.error('Failed to link match:', err);
toast({
title: 'Článek vytvořen, ale propojení se zápasem selhalo',
description: err?.response?.data?.error || err?.message || 'Zkuste propojit zápas ručně',
status: 'warning',
duration: 5000,
isClosable: true
});
}
} else if (created?.id) {
// No match to link, just show success
toast({
title: 'Článek byl úspěšně vytvořen',
status: 'success',
duration: 3000,
isClosable: true
});
}
}
// Close modal after successful save (unless keepOpen is true)
if (!options.keepOpen) {
closeModal();
}
} catch (error: any) {
@@ -1781,13 +1839,30 @@ const ArticlesAdminPage = () => {
qc.invalidateQueries({ queryKey: ['linked-polls'] });
}} />
) : (
<Alert status="info" borderRadius="md">
<Alert status="warning" borderRadius="md">
<AlertIcon />
<VStack align="start" spacing={1}>
<Text fontWeight="semibold">Nejprve uložte článek</Text>
<VStack align="start" spacing={2}>
<Text fontWeight="semibold">Článek ještě není uložen</Text>
<Text fontSize="sm">
Pro vytvoření nebo propojení ankety nejprve uložte článek tlačítkem "Uložit" níže. Poté se vrátíte do úprav a budete moci přidat ankety.
Pro propojení anket s článkem musíte nejprve článek uložit. Klikněte na "Uložit" níže - článek se uloží jako koncept a poté budete moci přidat ankety.
</Text>
<Button
size="sm"
colorScheme="blue"
onClick={async () => {
// Save article as draft first, keep modal open
try {
await onSubmit({ keepOpen: true });
// Switch to poll tab after save
setActiveTabIndex(5); // Poll tab is index 5
} catch (error) {
// Error is handled by onSubmit
}
}}
isLoading={createMut.isLoading}
>
Uložit jako koncept a přidat ankety
</Button>
</VStack>
</Alert>
)}
@@ -1798,7 +1873,7 @@ const ArticlesAdminPage = () => {
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={closeModal}>Zrušit</Button>
<Button colorScheme="blue" onClick={onSubmit} isLoading={createMut.isLoading || updateMut.isLoading}>Uložit</Button>
<Button colorScheme="blue" onClick={() => onSubmit()} isLoading={createMut.isLoading || updateMut.isLoading}>Uložit</Button>
</ModalFooter>
</ModalContent>
</Modal>