import { Box, Button, Heading, HStack, IconButton, Image, Input, InputGroup, InputLeftElement, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, SimpleGrid, useDisclosure, useToast, VStack, Text, Badge, Tabs, TabList, Tab, TabPanels, TabPanel, useColorModeValue, Tooltip, Select, Flex, Spacer, Stack, AspectRatio, Divider, Code, Skeleton, } from '@chakra-ui/react'; import { useState, useMemo } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FiTrash2, FiSearch, FiRefreshCw, FiCopy, FiUpload, FiImage, FiVideo, FiFile, FiDownload, FiExternalLink } from 'react-icons/fi'; import AdminLayout from '../../layouts/AdminLayout'; import { FileInfo, getAllFiles, deleteFile, scanAndSyncFiles, formatFileSize, } from '../../services/files'; import { uploadFile } from '../../services/articles'; const MediaAdminPage: React.FC = () => { const toast = useToast(); const qc = useQueryClient(); const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState<'all' | 'images' | 'videos' | 'documents'>('all'); const [selectedFile, setSelectedFile] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [uploadFiles, setUploadFiles] = useState(null); const [uploading, setUploading] = useState(false); const { isOpen: isDetailOpen, onOpen: onDetailOpen, onClose: onDetailClose } = useDisclosure(); const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure(); const { isOpen: isUploadOpen, onOpen: onUploadOpen, onClose: onUploadClose } = useDisclosure(); const borderColor = useColorModeValue('gray.200', 'gray.600'); const bgHover = useColorModeValue('gray.50', 'gray.700'); const cardBg = useColorModeValue('white', 'gray.800'); // Build MIME filter based on type const mimeFilter = useMemo(() => { if (typeFilter === 'images') return 'image/'; if (typeFilter === 'videos') return 'video/'; if (typeFilter === 'documents') return 'application/'; return ''; }, [typeFilter]); // Fetch all files const { data: allFiles = [], isLoading, refetch } = useQuery({ queryKey: ['admin-media-files', search, mimeFilter], queryFn: () => getAllFiles({ search, mime_type: mimeFilter }), }); // Filter files by type const filteredFiles = useMemo(() => { return allFiles.filter(file => { if (search && !file.filename?.toLowerCase().includes(search.toLowerCase())) { return false; } return true; }); }, [allFiles, search]); // Separate by media type const imageFiles = filteredFiles.filter(f => f.mime_type?.startsWith('image/')); const videoFiles = filteredFiles.filter(f => f.mime_type?.startsWith('video/')); const documentFiles = filteredFiles.filter(f => f.mime_type?.startsWith('application/') || f.mime_type?.startsWith('text/') ); // Delete mutation const deleteMutation = useMutation({ mutationFn: ({ id, force }: { id: number; force: boolean }) => deleteFile(id, force), onSuccess: () => { toast({ title: 'Soubor smazán', status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-media-files'] }); onDeleteClose(); setDeleteTarget(null); }, onError: (error: any) => { toast({ title: 'Chyba při mazání', description: error?.response?.data?.error || 'Nepodařilo se smazat soubor', status: 'error', }); }, }); // Scan mutation const scanMutation = useMutation({ mutationFn: scanAndSyncFiles, onSuccess: (data) => { toast({ title: 'Skenování dokončeno', description: `Přidáno: ${data.new_files || 0}, Smazáno: ${data.orphaned_files || 0}`, status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-media-files'] }); }, onError: () => { toast({ title: 'Chyba při skenování', status: 'error' }); }, }); const handleDelete = (file: FileInfo) => { setDeleteTarget(file); onDeleteOpen(); }; const confirmDelete = () => { if (deleteTarget) { deleteMutation.mutate({ id: deleteTarget.id, force: false }); } }; const handleViewDetails = (file: FileInfo) => { setSelectedFile(file); onDetailOpen(); }; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); toast({ title: 'Zkopírováno do schránky', status: 'success', duration: 2000 }); }; const handleUpload = async () => { if (!uploadFiles || uploadFiles.length === 0) return; setUploading(true); try { const promises = Array.from(uploadFiles).map(file => uploadFile(file)); await Promise.all(promises); toast({ title: 'Soubory nahrány', description: `Úspěšně nahráno ${uploadFiles.length} souborů`, status: 'success' }); qc.invalidateQueries({ queryKey: ['admin-media-files'] }); onUploadClose(); setUploadFiles(null); } catch (error) { toast({ title: 'Chyba při nahrávání', status: 'error' }); } finally { setUploading(false); } }; const getFileUrl = (file: FileInfo) => { if (file.url.startsWith('http')) return file.url; const base = window.location.origin; return `${base}${file.url}`; }; const MediaCard = ({ file }: { file: FileInfo }) => { const isImage = file.mime_type?.startsWith('image/'); const isVideo = file.mime_type?.startsWith('video/'); return ( handleViewDetails(file)} _hover={{ shadow: 'md', transform: 'translateY(-2px)' }} transition="all 0.2s" > {isImage ? ( {file.filename} ) : isVideo ? ( ) : ( )} {file.filename} {formatFileSize(file.size || 0)} {file.mime_type?.split('/')[0]} } size="xs" variant="ghost" onClick={(e) => { e.stopPropagation(); copyToClipboard(getFileUrl(file)); }} /> } size="xs" variant="ghost" colorScheme="red" onClick={(e) => { e.stopPropagation(); handleDelete(file); }} /> ); }; return ( Média Správa obrázků, videí a dalších souborů {/* Stats */} {imageFiles.length} Obrázků {videoFiles.length} Videí {documentFiles.length} Dokumentů {/* Filters */} setSearch(e.target.value)} /> {/* Tabs */} Všechny ({filteredFiles.length}) Obrázky ({imageFiles.length}) Videa ({videoFiles.length}) Dokumenty ({documentFiles.length}) {/* All Files */} {isLoading ? ( {[...Array(8)].map((_, i) => ( ))} ) : filteredFiles.length === 0 ? ( Žádné soubory ) : ( {filteredFiles.map(file => ( ))} )} {/* Images */} {imageFiles.length === 0 ? ( Žádné obrázky ) : ( {imageFiles.map(file => ( ))} )} {/* Videos */} {videoFiles.length === 0 ? ( Žádná videa ) : ( {videoFiles.map(file => ( ))} )} {/* Documents */} {documentFiles.length === 0 ? ( Žádné dokumenty ) : ( {documentFiles.map(file => ( ))} )} {/* File Details Modal */} Detail souboru {selectedFile && ( {selectedFile.mime_type?.startsWith('image/') && ( {selectedFile.filename} )} Název: {selectedFile.filename} Velikost: {formatFileSize(selectedFile.size || 0)} Typ: {selectedFile.mime_type} Vytvořeno: {selectedFile.created_at ? new Date(selectedFile.created_at).toLocaleString('cs-CZ') : 'N/A'} URL: {getFileUrl(selectedFile)} } size="sm" onClick={() => copyToClipboard(getFileUrl(selectedFile))} /> )} {selectedFile && ( )} {/* Delete Confirmation Modal */} Smazat soubor Opravdu chcete smazat soubor {deleteTarget?.filename}? Tato akce je nevratná. {/* Upload Modal */} Nahrát soubory Vyberte jeden nebo více souborů k nahrání. setUploadFiles(e.target.files)} p={1} /> {uploadFiles && uploadFiles.length > 0 && ( Vybrané soubory ({uploadFiles.length}): {Array.from(uploadFiles).map((file, i) => ( • {file.name} ({formatFileSize(file.size)}) ))} )} ); }; export default MediaAdminPage;