mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #63
This commit is contained in:
@@ -142,13 +142,8 @@ const MapLinkImporter: React.FC<MapLinkImporterProps> = ({
|
||||
onChange={handleUrlChange}
|
||||
size="md"
|
||||
/>
|
||||
<FormHelperText>
|
||||
Podporované formáty:
|
||||
<Text as="span" fontWeight="semibold" ml={1}>mapy.cz</Text> (mapy.com/en/letecka?x=...&y=...),
|
||||
<Text as="span" fontWeight="semibold" ml={1}>Google Maps</Text> (google.com/maps/place/@lat,lng,zoom)
|
||||
</FormHelperText>
|
||||
<HStack mt={2} spacing={3} fontSize="sm">
|
||||
<Text color="gray.600">Quick links:</Text>
|
||||
<Text color="gray.600">Rychlé odkazy:</Text>
|
||||
<Link
|
||||
href="https://mapy.com/cs/"
|
||||
isExternal
|
||||
@@ -311,27 +306,6 @@ const MapLinkImporter: React.FC<MapLinkImporterProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Example URLs */}
|
||||
<Box
|
||||
bg={bgColor}
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
fontSize="sm"
|
||||
>
|
||||
<Text fontWeight="semibold" mb={2}>Příklady podporovaných URL:</Text>
|
||||
<VStack align="start" spacing={1}>
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
<strong>Mapy.cz:</strong><br />
|
||||
mapy.cz/en/letecka?x=17.6996859&y=50.0947150&z=19
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
<strong>Google Maps:</strong><br />
|
||||
google.com/maps/place/@50.0948669,17.7001456,226m
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -71,7 +71,6 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
const quillRef = useRef<ReactQuill | null>(null);
|
||||
const [editorMode, setEditorMode] = useState<'rich' | 'html'>('rich');
|
||||
|
||||
// Crop modal state
|
||||
const [cropOpen, setCropOpen] = useState(false);
|
||||
@@ -113,8 +112,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ align: [] }],
|
||||
['link', 'image', 'video'],
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image'],
|
||||
['blockquote'],
|
||||
['clean'],
|
||||
],
|
||||
basic: [
|
||||
@@ -369,11 +368,22 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Show toolbar and position it
|
||||
// Show toolbar and position it above the image
|
||||
const rect = img.getBoundingClientRect();
|
||||
const editorRect = editor.root.getBoundingClientRect();
|
||||
const scrollTop = editor.root.scrollTop;
|
||||
const toolbarHeight = 400; // Approximate toolbar height
|
||||
|
||||
// Calculate position relative to editor, accounting for scroll
|
||||
let topPos = rect.top - editorRect.top + scrollTop - 60;
|
||||
|
||||
// If toolbar would go above visible area, position it below the image
|
||||
if (topPos < scrollTop) {
|
||||
topPos = rect.bottom - editorRect.top + scrollTop + 10;
|
||||
}
|
||||
|
||||
setToolbarPosition({
|
||||
top: rect.top - editorRect.top - 50,
|
||||
top: topPos,
|
||||
left: rect.left - editorRect.left,
|
||||
});
|
||||
setShowImageToolbar(true);
|
||||
@@ -533,55 +543,52 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
return (
|
||||
<Box>
|
||||
{/* Editor Controls */}
|
||||
{!readOnly && (
|
||||
<HStack mb={2} spacing={2} justify="space-between" flexWrap="wrap">
|
||||
<ButtonGroup size="sm" isAttached variant="outline">
|
||||
<Button
|
||||
leftIcon={<Type size={16} />}
|
||||
variant={editorMode === 'rich' ? 'solid' : 'outline'}
|
||||
colorScheme={editorMode === 'rich' ? 'blue' : 'gray'}
|
||||
onClick={() => setEditorMode('rich')}
|
||||
>
|
||||
Editor
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<Code size={16} />}
|
||||
variant={editorMode === 'html' ? 'solid' : 'outline'}
|
||||
colorScheme={editorMode === 'html' ? 'blue' : 'gray'}
|
||||
onClick={() => setEditorMode('html')}
|
||||
>
|
||||
HTML
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
{editorMode === 'rich' && onImageUpload && (
|
||||
<Button
|
||||
size="sm"
|
||||
leftIcon={<ImageIcon size={16} />}
|
||||
colorScheme="purple"
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
Vložit obrázek
|
||||
</Button>
|
||||
)}
|
||||
{!readOnly && onImageUpload && (
|
||||
<HStack mb={2} spacing={2} justify="flex-start" flexWrap="wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
leftIcon={<ImageIcon size={16} />}
|
||||
colorScheme="purple"
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
Vložit obrázek
|
||||
</Button>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
nebo použijte tlačítko obrázku v nástrojové liště
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{editorMode === 'rich' ? (
|
||||
<Box
|
||||
position="relative"
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
bg={bgColor}
|
||||
sx={{
|
||||
<Box
|
||||
position="relative"
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
bg={bgColor}
|
||||
sx={{
|
||||
'.ql-toolbar': {
|
||||
borderBottom: '1px solid',
|
||||
borderColor: borderColor,
|
||||
bg: hoverBg,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
padding: '12px',
|
||||
'& button': {
|
||||
color: 'gray.700 !important',
|
||||
width: '32px !important',
|
||||
height: '32px !important',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
background: 'rgba(49, 130, 206, 0.1) !important',
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
'&.ql-active': {
|
||||
background: 'rgba(49, 130, 206, 0.2) !important',
|
||||
color: '#3182ce !important',
|
||||
},
|
||||
},
|
||||
'& .ql-stroke': {
|
||||
stroke: 'gray.700 !important',
|
||||
@@ -589,6 +596,29 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
'& .ql-fill': {
|
||||
fill: 'gray.700 !important',
|
||||
},
|
||||
'& .ql-active .ql-stroke': {
|
||||
stroke: '#3182ce !important',
|
||||
},
|
||||
'& .ql-active .ql-fill': {
|
||||
fill: '#3182ce !important',
|
||||
},
|
||||
'& .ql-picker': {
|
||||
color: 'gray.700 !important',
|
||||
},
|
||||
'& .ql-picker-label': {
|
||||
borderRadius: '6px',
|
||||
padding: '4px 8px',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
background: 'rgba(49, 130, 206, 0.1) !important',
|
||||
},
|
||||
},
|
||||
'& .ql-picker-options': {
|
||||
background: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
padding: '8px',
|
||||
},
|
||||
},
|
||||
'.ql-container': {
|
||||
fontSize: '16px',
|
||||
@@ -601,6 +631,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
overflowY: 'auto',
|
||||
bg: 'white !important',
|
||||
color: 'gray.800 !important',
|
||||
padding: '16px',
|
||||
lineHeight: '1.6',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
},
|
||||
@@ -611,6 +643,27 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
bg: 'gray.400',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'h1': {
|
||||
fontSize: '2em !important',
|
||||
fontWeight: 'bold !important',
|
||||
marginTop: '0.67em !important',
|
||||
marginBottom: '0.67em !important',
|
||||
lineHeight: '1.2 !important',
|
||||
},
|
||||
'h2': {
|
||||
fontSize: '1.5em !important',
|
||||
fontWeight: 'bold !important',
|
||||
marginTop: '0.83em !important',
|
||||
marginBottom: '0.83em !important',
|
||||
lineHeight: '1.3 !important',
|
||||
},
|
||||
'h3': {
|
||||
fontSize: '1.17em !important',
|
||||
fontWeight: 'bold !important',
|
||||
marginTop: '1em !important',
|
||||
marginBottom: '1em !important',
|
||||
lineHeight: '1.4 !important',
|
||||
},
|
||||
img: {
|
||||
cursor: 'pointer',
|
||||
maxWidth: '100%',
|
||||
@@ -652,26 +705,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
as="textarea"
|
||||
value={value}
|
||||
onChange={(e: any) => onChange(e.target.value)}
|
||||
fontFamily="mono"
|
||||
fontSize="sm"
|
||||
p={4}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
bg={bgColor}
|
||||
resize="vertical"
|
||||
minH={height}
|
||||
maxH="70vh"
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!readOnly && editorMode === 'rich' && (
|
||||
{!readOnly && (
|
||||
<Text fontSize="xs" color="gray.500" mt={2}>
|
||||
💡 Tip: Klikněte na obrázek pro výběr a úpravu. Používejte nástrojovou lištu pro filtry a transformace.
|
||||
</Text>
|
||||
@@ -684,14 +719,16 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
top={`${toolbarPosition.top}px`}
|
||||
left={`${toolbarPosition.left}px`}
|
||||
bg={toolbarBg}
|
||||
borderWidth="1px"
|
||||
borderColor={toolbarBorder}
|
||||
borderWidth="2px"
|
||||
borderColor="blue.400"
|
||||
borderRadius="lg"
|
||||
boxShadow="lg"
|
||||
p={3}
|
||||
zIndex={1500}
|
||||
minW="320px"
|
||||
maxW="400px"
|
||||
boxShadow="2xl"
|
||||
p={4}
|
||||
zIndex={9999}
|
||||
minW="340px"
|
||||
maxW="420px"
|
||||
pointerEvents="auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{/* Toolbar Header */}
|
||||
@@ -894,7 +931,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
>
|
||||
<img
|
||||
ref={imgRef as any}
|
||||
src={cropSrc}
|
||||
src={cropSrc || ''}
|
||||
alt="Crop preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
HStack,
|
||||
Icon,
|
||||
Link as ChakraLink,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalFooter,
|
||||
Text,
|
||||
useDisclosure,
|
||||
Image,
|
||||
VStack,
|
||||
Badge,
|
||||
useColorModeValue,
|
||||
AspectRatio,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FiDownload,
|
||||
FiEye,
|
||||
FiFile,
|
||||
FiFileText,
|
||||
FiImage,
|
||||
FiVideo,
|
||||
FiMusic,
|
||||
} from 'react-icons/fi';
|
||||
import { assetUrl } from '../../utils/url';
|
||||
|
||||
export interface FilePreviewProps {
|
||||
url: string;
|
||||
name?: string;
|
||||
mimeType?: string;
|
||||
size?: number;
|
||||
showInline?: boolean;
|
||||
}
|
||||
|
||||
const FilePreview: React.FC<FilePreviewProps> = ({
|
||||
url,
|
||||
name,
|
||||
mimeType = '',
|
||||
size,
|
||||
showInline = false,
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
const fullUrl = assetUrl(url) || url;
|
||||
const fileName = name || url.split('/').pop() || 'file';
|
||||
const mime = mimeType.toLowerCase();
|
||||
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const mutedText = useColorModeValue('gray.600', 'gray.300');
|
||||
const linkColor = useColorModeValue('blue.600', 'blue.300');
|
||||
|
||||
// Determine file type and icon
|
||||
const getFileInfo = () => {
|
||||
if (mime.startsWith('image/')) {
|
||||
return { type: 'image', icon: FiImage, color: 'purple.500', canPreview: true };
|
||||
}
|
||||
if (mime === 'application/pdf') {
|
||||
return { type: 'pdf', icon: FiFileText, color: 'red.500', canPreview: true };
|
||||
}
|
||||
if (mime.startsWith('video/')) {
|
||||
return { type: 'video', icon: FiVideo, color: 'pink.500', canPreview: true };
|
||||
}
|
||||
if (mime.startsWith('audio/')) {
|
||||
return { type: 'audio', icon: FiMusic, color: 'green.500', canPreview: true };
|
||||
}
|
||||
if (mime.includes('word') || mime.includes('document')) {
|
||||
return { type: 'document', icon: FiFileText, color: 'blue.500', canPreview: false };
|
||||
}
|
||||
if (mime.includes('sheet') || mime.includes('excel')) {
|
||||
return { type: 'spreadsheet', icon: FiFile, color: 'green.600', canPreview: false };
|
||||
}
|
||||
if (mime.includes('presentation') || mime.includes('powerpoint')) {
|
||||
return { type: 'presentation', icon: FiFile, color: 'orange.500', canPreview: false };
|
||||
}
|
||||
return { type: 'other', icon: FiFile, color: 'gray.500', canPreview: false };
|
||||
};
|
||||
|
||||
const fileInfo = getFileInfo();
|
||||
const sizeKB = typeof size === 'number' ? Math.round(size / 1024) : undefined;
|
||||
const sizeMB = sizeKB && sizeKB > 1024 ? (sizeKB / 1024).toFixed(1) : undefined;
|
||||
const sizeStr = sizeMB ? `${sizeMB} MB` : sizeKB ? `${sizeKB} kB` : '';
|
||||
|
||||
// Render preview content based on file type
|
||||
const renderPreviewContent = () => {
|
||||
if (fileInfo.type === 'image') {
|
||||
if (imageError) {
|
||||
return (
|
||||
<VStack spacing={4} py={10}>
|
||||
<Icon as={FiImage} boxSize={12} color="gray.400" />
|
||||
<Text color={mutedText}>Obrázek se nepodařilo načíst</Text>
|
||||
<Button
|
||||
as={ChakraLink}
|
||||
href={fullUrl}
|
||||
isExternal
|
||||
leftIcon={<FiDownload />}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Stáhnout soubor
|
||||
</Button>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Image
|
||||
src={fullUrl}
|
||||
alt={fileName}
|
||||
maxW="100%"
|
||||
maxH="70vh"
|
||||
objectFit="contain"
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fileInfo.type === 'pdf') {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (fileInfo.type === 'video') {
|
||||
return (
|
||||
<AspectRatio ratio={16 / 9} w="100%">
|
||||
<video controls style={{ width: '100%', height: '100%' }}>
|
||||
<source src={fullUrl} type={mime} />
|
||||
Váš prohlížeč nepodporuje přehrávání videa.
|
||||
</video>
|
||||
</AspectRatio>
|
||||
);
|
||||
}
|
||||
|
||||
if (fileInfo.type === 'audio') {
|
||||
return (
|
||||
<VStack spacing={4} py={10}>
|
||||
<Icon as={FiMusic} boxSize={12} color={fileInfo.color} />
|
||||
<audio controls style={{ width: '100%', maxWidth: '500px' }}>
|
||||
<source src={fullUrl} type={mime} />
|
||||
Váš prohlížeč nepodporuje přehrávání zvuku.
|
||||
</audio>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
// For Office documents, show info and download option
|
||||
return (
|
||||
<VStack spacing={4} py={10}>
|
||||
<Icon as={fileInfo.icon} boxSize={16} color={fileInfo.color} />
|
||||
<VStack spacing={2}>
|
||||
<Text fontSize="lg" fontWeight="medium">{fileName}</Text>
|
||||
{sizeStr && <Badge colorScheme="gray">{sizeStr}</Badge>}
|
||||
<Text color={mutedText} fontSize="sm" textAlign="center">
|
||||
{fileInfo.type === 'presentation' && 'PowerPoint prezentace'}
|
||||
{fileInfo.type === 'document' && 'Word dokument'}
|
||||
{fileInfo.type === 'spreadsheet' && 'Excel tabulka'}
|
||||
</Text>
|
||||
</VStack>
|
||||
<HStack spacing={3}>
|
||||
<Button
|
||||
as={ChakraLink}
|
||||
href={fullUrl}
|
||||
isExternal
|
||||
leftIcon={<FiDownload />}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Stáhnout
|
||||
</Button>
|
||||
<Button
|
||||
as={ChakraLink}
|
||||
href={`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fullUrl)}`}
|
||||
isExternal
|
||||
leftIcon={<FiEye />}
|
||||
variant="outline"
|
||||
>
|
||||
Zobrazit online
|
||||
</Button>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={mutedText}>
|
||||
Pro zobrazení .pptx, .docx, .xlsx můžete použít "Zobrazit online"
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
// Inline preview for images
|
||||
if (showInline && fileInfo.type === 'image') {
|
||||
return (
|
||||
<Box
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
bg={cardBg}
|
||||
>
|
||||
<Image
|
||||
src={fullUrl}
|
||||
alt={fileName}
|
||||
w="100%"
|
||||
maxH="400px"
|
||||
objectFit="cover"
|
||||
cursor="pointer"
|
||||
onClick={onOpen}
|
||||
_hover={{ opacity: 0.9 }}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
{!imageError && (
|
||||
<HStack justify="space-between" p={3} borderTopWidth="1px">
|
||||
<Text fontSize="sm" color={mutedText} isTruncated maxW="60%">
|
||||
{fileName}
|
||||
</Text>
|
||||
<HStack spacing={2}>
|
||||
<Button size="sm" leftIcon={<FiEye />} onClick={onOpen}>
|
||||
Náhled
|
||||
</Button>
|
||||
<Button
|
||||
as={ChakraLink}
|
||||
href={fullUrl}
|
||||
isExternal
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
leftIcon={<FiDownload />}
|
||||
>
|
||||
Stáhnout
|
||||
</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Compact button view
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
justify="space-between"
|
||||
p={3}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
bg={cardBg}
|
||||
>
|
||||
<HStack flex={1} minW={0}>
|
||||
<Icon as={fileInfo.icon} color={fileInfo.color} flexShrink={0} />
|
||||
<VStack align="start" spacing={0} flex={1} minW={0}>
|
||||
<ChakraLink
|
||||
href={fullUrl}
|
||||
isExternal
|
||||
color={linkColor}
|
||||
fontWeight="medium"
|
||||
isTruncated
|
||||
maxW="100%"
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{fileName}
|
||||
</ChakraLink>
|
||||
{sizeStr && <Text fontSize="xs" color={mutedText}>{sizeStr}</Text>}
|
||||
</VStack>
|
||||
</HStack>
|
||||
<HStack spacing={2} flexShrink={0}>
|
||||
{fileInfo.canPreview && (
|
||||
<Button size="sm" leftIcon={<FiEye />} onClick={onOpen} variant="outline">
|
||||
Náhled
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as={ChakraLink}
|
||||
href={fullUrl}
|
||||
isExternal
|
||||
size="sm"
|
||||
leftIcon={<FiDownload />}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Stáhnout
|
||||
</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* Preview Modal */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="6xl" isCentered>
|
||||
<ModalOverlay bg="blackAlpha.800" />
|
||||
<ModalContent maxW="90vw" maxH="90vh">
|
||||
<ModalHeader>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text>{fileName}</Text>
|
||||
{sizeStr && <Text fontSize="sm" fontWeight="normal" color={mutedText}>{sizeStr}</Text>}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6} overflow="auto">
|
||||
{renderPreviewContent()}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
as={ChakraLink}
|
||||
href={fullUrl}
|
||||
isExternal
|
||||
leftIcon={<FiDownload />}
|
||||
colorScheme="blue"
|
||||
mr={3}
|
||||
>
|
||||
Stáhnout
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Zavřít
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePreview;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Image,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverBody,
|
||||
useColorModeValue,
|
||||
Portal,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
export interface ThumbnailPreviewProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
size?: string;
|
||||
previewSize?: string;
|
||||
borderRadius?: string;
|
||||
objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
|
||||
}
|
||||
|
||||
/**
|
||||
* ThumbnailPreview - Small thumbnail image with hover to show larger preview
|
||||
* Perfect for admin table rows where you want to see images without clicking
|
||||
*/
|
||||
const ThumbnailPreview: React.FC<ThumbnailPreviewProps> = ({
|
||||
src,
|
||||
alt,
|
||||
size = '48px',
|
||||
previewSize = '300px',
|
||||
borderRadius = 'md',
|
||||
objectFit = 'cover',
|
||||
}) => {
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const bgColor = useColorModeValue('white', 'gray.800');
|
||||
|
||||
return (
|
||||
<Popover trigger="hover" placement="right" openDelay={200} closeDelay={100}>
|
||||
<PopoverTrigger>
|
||||
<Box
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
_hover={{ transform: 'scale(1.05)', boxShadow: 'md' }}
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
boxSize={size}
|
||||
objectFit={objectFit}
|
||||
borderRadius={borderRadius}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
loading="lazy"
|
||||
/>
|
||||
</Box>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent
|
||||
width={previewSize}
|
||||
borderColor={borderColor}
|
||||
boxShadow="2xl"
|
||||
bg={bgColor}
|
||||
_focus={{ boxShadow: '2xl' }}
|
||||
>
|
||||
<PopoverBody p={0}>
|
||||
<Image
|
||||
src={src}
|
||||
alt={`${alt} - preview`}
|
||||
width="100%"
|
||||
maxH="400px"
|
||||
objectFit="contain"
|
||||
borderRadius="md"
|
||||
loading="lazy"
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThumbnailPreview;
|
||||
@@ -118,6 +118,9 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const clubTheme = useClubTheme();
|
||||
|
||||
// Early return if not admin - MUST be before any other hooks
|
||||
if (!isAdmin) return null;
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [configs, setConfigs] = useState<PageElementConfig[]>([]);
|
||||
const [localChanges, setLocalChanges] = useState<Record<string, string>>({});
|
||||
@@ -148,7 +151,6 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
|
||||
// Auto-activate editing mode if URL parameter is present
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('myuibrix') === 'edit') {
|
||||
setIsEditing(true);
|
||||
@@ -164,12 +166,11 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, [isAdmin, toast]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Load configurations
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return;
|
||||
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
const data = await getPageElementConfigs(pageType);
|
||||
@@ -225,7 +226,7 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
};
|
||||
|
||||
loadConfigs();
|
||||
}, [pageType, isAdmin]);
|
||||
}, [pageType]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
@@ -630,8 +631,6 @@ const MyUIbrixStyleEditor: React.FC<MyUIbrixStyleEditorProps> = ({ pageType, onC
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAdmin) return null;
|
||||
|
||||
const currentVariants = selectedElement ? ELEMENT_VARIANTS[selectedElement] : [];
|
||||
const currentVariant = selectedElement ? (localChanges[selectedElement] || currentVariants[0]?.value) : null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user