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