This commit is contained in:
Tomáš Dvořák
2025-10-16 17:10:13 +02:00
parent f5e7be92c7
commit 35d0954afd
84 changed files with 9571 additions and 4668 deletions
@@ -220,6 +220,8 @@ const AnalyticsAdminPage: React.FC = () => {
} | null>(null);
const [countryDetails, setCountryDetails] = useState<any>(null);
const [loadingCountryDetails, setLoadingCountryDetails] = useState(false);
const [umamiConfig, setUmamiConfig] = useState<any>(null);
const [showDiagnostics, setShowDiagnostics] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const bgColor = useColorModeValue('white', 'gray.800');
@@ -310,8 +312,18 @@ const AnalyticsAdminPage: React.FC = () => {
useEffect(() => {
fetchAnalytics(timeRange);
fetchUmamiConfig();
}, [timeRange]);
const fetchUmamiConfig = async () => {
try {
const response = await api.get('/umami/config');
setUmamiConfig(response.data);
} catch (error) {
console.error('Failed to fetch Umami config:', error);
}
};
const handleCountryClick = async (countryCode: string, countryName: string, value: number) => {
setSelectedCountry({ code: countryCode, name: countryName, value });
setLoadingCountryDetails(true);
@@ -544,6 +556,117 @@ const AnalyticsAdminPage: React.FC = () => {
</Card>
</SimpleGrid>
{/* Diagnostics Panel */}
{(!hasData || showDiagnostics) && (
<Card bg="blue.50" borderColor="blue.300" borderWidth={2}>
<CardBody>
<HStack spacing={3} align="start">
<Icon as={FiActivity} color="blue.500" boxSize={6} mt={1} />
<VStack align="start" spacing={3} flex={1}>
<HStack justify="space-between" w="full">
<Text fontWeight="bold" color="blue.800" fontSize="lg">Diagnostika analytiky</Text>
<Button
size="xs"
variant="ghost"
onClick={() => setShowDiagnostics(!showDiagnostics)}
>
{showDiagnostics ? 'Skrýt' : 'Zobrazit detaily'}
</Button>
</HStack>
{/* Umami Connection Status */}
<Box w="full">
<HStack spacing={2} mb={2}>
<Badge colorScheme={umamiConfig?.enabled ? 'green' : 'red'}>
{umamiConfig?.enabled ? 'Připojeno' : 'Nepřipojeno'}
</Badge>
<Text fontSize="sm" fontWeight="semibold" color="blue.800">
Stav Umami
</Text>
</HStack>
{umamiConfig && (
<VStack align="start" spacing={1} pl={4}>
<Text fontSize="xs" color="blue.700">
<strong>Aktivováno:</strong> {umamiConfig.enabled ? 'Ano' : 'Ne'}
</Text>
{umamiConfig.website_id && (
<Text fontSize="xs" color="blue.700">
<strong>Website ID:</strong> {umamiConfig.website_id}
</Text>
)}
{umamiConfig.reason && (
<Text fontSize="xs" color="red.600">
<strong>Důvod:</strong> {umamiConfig.reason}
</Text>
)}
</VStack>
)}
</Box>
<Divider borderColor="blue.200" />
{/* Why No Data */}
{!hasData && (
<>
<Text fontSize="sm" color="blue.800" fontWeight="semibold">
Proč nejsou k dispozici žádná data?
</Text>
<VStack align="start" spacing={1} pl={4}>
<Text fontSize="xs" color="blue.700">
Umami tracking ještě nezaznamenal žádné návštěvy
</Text>
<Text fontSize="xs" color="blue.700">
Tracking script se načítá pouze na veřejných stránkách (ne na /admin)
</Text>
<Text fontSize="xs" color="blue.700">
Data se aktualizují v reálném čase po návštěvě veřejných stránek
</Text>
</VStack>
<Divider borderColor="blue.200" />
<Text fontSize="sm" color="blue.800" fontWeight="semibold">
Jak vygenerovat testovací data:
</Text>
<VStack align="start" spacing={1} pl={4}>
<Text fontSize="xs" color="blue.700">
1. Otevřete hlavní stránku webu v novém okně inkognito
</Text>
<Text fontSize="xs" color="blue.700">
2. Procházejte několik veřejných stránek (Blog, O klubu, Kontakt...)
</Text>
<Text fontSize="xs" color="blue.700">
3. Počkejte 1-2 minuty a obnovte tuto stránku analytiky
</Text>
</VStack>
<HStack spacing={2} mt={2}>
<Button
size="sm"
colorScheme="blue"
leftIcon={<Icon as={FiGlobe} />}
onClick={() => window.open('/', '_blank')}
>
Otevřít hlavní stránku
</Button>
<Button
size="sm"
colorScheme="blue"
variant="outline"
leftIcon={<Icon as={FiZap} />}
onClick={() => window.location.reload()}
>
Obnovit analytiku
</Button>
</HStack>
</>
)}
</VStack>
</HStack>
</CardBody>
</Card>
)}
{/* Error Message */}
{errorMessage && (
<Card bg="orange.50" borderColor="orange.300" borderWidth={2}>
+514
View File
@@ -0,0 +1,514 @@
/**
* DevDocsPage - Admin Documentation Viewer
*
* REQUIRED DEPENDENCIES:
* npm install react-markdown react-syntax-highlighter
* npm install --save-dev @types/react-syntax-highlighter
*
* This component requires these packages to render markdown with syntax highlighting.
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Heading,
Text,
VStack,
HStack,
Button,
Input,
InputGroup,
InputLeftElement,
Select,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Badge,
Icon,
useColorModeValue,
Divider,
Code,
Alert,
AlertIcon,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
Spinner,
useToast,
} from '@chakra-ui/react';
import {
FiSearch,
FiBook,
FiCode,
FiFileText,
FiLayers,
FiTool,
FiHome,
FiDownload,
FiRefreshCw,
} from 'react-icons/fi';
import { Link as RouterLink } from 'react-router-dom';
// @ts-ignore - Install with: npm install react-markdown
import ReactMarkdown from 'react-markdown';
// @ts-ignore - Install with: npm install react-syntax-highlighter
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
// @ts-ignore
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
interface DocFile {
name: string;
path: string;
category: string;
description: string;
icon: any;
tags: string[];
}
const DevDocsPage: React.FC = () => {
const [selectedDoc, setSelectedDoc] = useState<string>('');
const [docContent, setDocContent] = useState<string>('');
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [loading, setLoading] = useState(false);
const toast = useToast();
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const sidebarBg = useColorModeValue('gray.50', 'gray.900');
// Documentation files registry
const docFiles: DocFile[] = [
{
name: 'MyUIbrix Elementor Features',
path: '/DOCS/MYUIBRIX_ELEMENTOR_FEATURES.md',
category: 'Features',
description: 'Complete guide to Elementor-style page builder features',
icon: FiLayers,
tags: ['myuibrix', 'elementor', 'editor', 'features'],
},
{
name: 'MyUIbrix Enhancement Summary',
path: '/DOCS/MYUIBRIX_ENHANCEMENT_SUMMARY.md',
category: 'Features',
description: 'Implementation summary of Elementor enhancements',
icon: FiTool,
tags: ['myuibrix', 'enhancement', 'summary'],
},
{
name: 'MyUIbrix Quick Start',
path: '/DOCS/MYUIBRIX_QUICK_START.md',
category: 'Guides',
description: 'Quick reference guide for MyUIbrix editor',
icon: FiBook,
tags: ['myuibrix', 'quick-start', 'guide'],
},
{
name: 'MyUIbrix Fixes',
path: '/DOCS/MYUIBRIX_FIXES.md',
category: 'Technical',
description: 'Technical fixes and improvements documentation',
icon: FiTool,
tags: ['myuibrix', 'fixes', 'technical'],
},
{
name: 'Integration Guide',
path: '/DOCS/INTEGRATION_GUIDE.md',
category: 'Development',
description: 'How to integrate MyUIbrix components',
icon: FiCode,
tags: ['integration', 'development', 'components'],
},
{
name: 'CSS Classes Reference',
path: '/DOCS/CSS_CLASSES_REFERENCE.md',
category: 'Reference',
description: 'Complete CSS classes and selectors reference',
icon: FiFileText,
tags: ['css', 'styling', 'classes', 'reference'],
},
{
name: 'Admin Functionality Report',
path: '/DOCS/ADMIN_FUNCTIONALITY_REPORT.md',
category: 'Admin',
description: 'Complete admin panel functionality documentation',
icon: FiTool,
tags: ['admin', 'functionality', 'report'],
},
{
name: 'Setup Improvements',
path: '/DOCS/SETUP_IMPROVEMENTS.md',
category: 'Setup',
description: 'Initial setup and configuration guide',
icon: FiBook,
tags: ['setup', 'configuration', 'improvements'],
},
{
name: 'Docker Enhancements',
path: '/DOCS/DOCKER_ENHANCEMENTS_SUMMARY.md',
category: 'DevOps',
description: 'Docker setup and deployment guide',
icon: FiCode,
tags: ['docker', 'deployment', 'devops'],
},
];
const categories = ['all', 'Features', 'Guides', 'Technical', 'Development', 'Reference', 'Admin', 'Setup', 'DevOps'];
// Filter documents
const filteredDocs = docFiles.filter(doc => {
const matchesSearch = searchQuery === '' ||
doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
doc.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
doc.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesCategory = selectedCategory === 'all' || doc.category === selectedCategory;
return matchesSearch && matchesCategory;
});
// Load document content
const loadDocument = async (docPath: string) => {
setLoading(true);
setSelectedDoc(docPath);
try {
// In production, fetch from backend API
const response = await fetch(docPath);
if (!response.ok) throw new Error('Failed to load document');
const content = await response.text();
setDocContent(content);
} catch (error) {
console.error('Error loading document:', error);
// Fallback: show error message
setDocContent(`# Document Not Found\n\nThe requested documentation file could not be loaded.\n\n**Path**: ${docPath}\n\nPlease ensure the documentation files are properly deployed.`);
toast({
title: 'Error loading document',
description: 'The documentation file could not be loaded',
status: 'error',
duration: 3000,
});
} finally {
setLoading(false);
}
};
// Load first document on mount
useEffect(() => {
if (docFiles.length > 0) {
loadDocument(docFiles[0].path);
}
}, []);
// Custom markdown components
const markdownComponents = {
code({ node, inline, className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<Code {...props}>{children}</Code>
);
},
h1: ({ children }: any) => (
<Heading as="h1" size="2xl" mb={6} mt={8}>
{children}
</Heading>
),
h2: ({ children }: any) => (
<Heading as="h2" size="xl" mb={4} mt={6}>
{children}
</Heading>
),
h3: ({ children }: any) => (
<Heading as="h3" size="lg" mb={3} mt={5}>
{children}
</Heading>
),
p: ({ children }: any) => (
<Text mb={4} lineHeight="tall">
{children}
</Text>
),
ul: ({ children }: any) => (
<VStack as="ul" align="stretch" spacing={2} mb={4} pl={6}>
{children}
</VStack>
),
li: ({ children }: any) => (
<Text as="li" mb={1}>
{children}
</Text>
),
};
// Download document
const downloadDocument = () => {
const blob = new Blob([docContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = selectedDoc.split('/').pop() || 'document.md';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: 'Document downloaded',
status: 'success',
duration: 2000,
});
};
return (
<Box minH="100vh" bg={useColorModeValue('gray.50', 'gray.900')}>
{/* Breadcrumb */}
<Box bg={bgColor} borderBottom="1px" borderColor={borderColor} py={4}>
<Container maxW="container.xl">
<Breadcrumb>
<BreadcrumbItem>
<BreadcrumbLink as={RouterLink} to="/admin">
<HStack spacing={2}>
<FiHome />
<Text>Admin</Text>
</HStack>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink>
<HStack spacing={2}>
<FiBook />
<Text>Developer Documentation</Text>
</HStack>
</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
</Container>
</Box>
{/* Header */}
<Box bg={bgColor} borderBottom="1px" borderColor={borderColor} py={6}>
<Container maxW="container.xl">
<VStack align="stretch" spacing={4}>
<HStack justify="space-between" align="start">
<VStack align="start" spacing={2}>
<Heading size="lg">📚 Developer Documentation</Heading>
<Text color="gray.600">
Complete technical documentation for MyUIbrix and admin features
</Text>
</VStack>
<HStack spacing={2}>
<Button
leftIcon={<FiDownload />}
size="sm"
variant="outline"
onClick={downloadDocument}
isDisabled={!selectedDoc}
>
Download
</Button>
<Button
leftIcon={<FiRefreshCw />}
size="sm"
variant="outline"
onClick={() => selectedDoc && loadDocument(selectedDoc)}
isLoading={loading}
>
Refresh
</Button>
</HStack>
</HStack>
{/* Search and Filter */}
<HStack spacing={4}>
<InputGroup maxW="400px">
<InputLeftElement pointerEvents="none">
<FiSearch color="gray.300" />
</InputLeftElement>
<Input
placeholder="Search documentation..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
bg={bgColor}
/>
</InputGroup>
<Select
maxW="200px"
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
bg={bgColor}
>
{categories.map(cat => (
<option key={cat} value={cat}>
{cat === 'all' ? 'All Categories' : cat}
</option>
))}
</Select>
<Badge colorScheme="blue" fontSize="md" px={3} py={1}>
{filteredDocs.length} docs
</Badge>
</HStack>
</VStack>
</Container>
</Box>
{/* Main Content */}
<Container maxW="container.xl" py={8}>
<HStack align="start" spacing={6}>
{/* Sidebar */}
<VStack
width="350px"
bg={sidebarBg}
borderRadius="lg"
p={4}
align="stretch"
spacing={3}
maxH="calc(100vh - 300px)"
overflowY="auto"
position="sticky"
top="20px"
>
<Text fontWeight="bold" fontSize="sm" textTransform="uppercase" color="gray.500">
Documentation Files
</Text>
{filteredDocs.length === 0 ? (
<Alert status="info" borderRadius="md">
<AlertIcon />
No documents found
</Alert>
) : (
filteredDocs.map((doc) => (
<Box
key={doc.path}
p={4}
bg={selectedDoc === doc.path ? 'blue.50' : bgColor}
borderRadius="md"
cursor="pointer"
transition="all 0.2s"
borderWidth="2px"
borderColor={selectedDoc === doc.path ? 'blue.400' : 'transparent'}
_hover={{
transform: 'translateX(4px)',
borderColor: 'blue.300',
}}
onClick={() => loadDocument(doc.path)}
>
<HStack spacing={3} mb={2}>
<Icon as={doc.icon} boxSize={5} color="blue.500" />
<VStack align="start" spacing={0} flex={1}>
<Text fontWeight="bold" fontSize="sm">
{doc.name}
</Text>
<Badge colorScheme="purple" fontSize="xs">
{doc.category}
</Badge>
</VStack>
</HStack>
<Text fontSize="xs" color="gray.600">
{doc.description}
</Text>
<HStack spacing={1} mt={2} flexWrap="wrap">
{doc.tags.slice(0, 3).map(tag => (
<Badge key={tag} size="sm" variant="outline" fontSize="xs">
{tag}
</Badge>
))}
</HStack>
</Box>
))
)}
</VStack>
{/* Content Area */}
<Box
flex={1}
bg={bgColor}
borderRadius="lg"
p={8}
boxShadow="sm"
minH="600px"
>
{loading ? (
<VStack spacing={4} py={12}>
<Spinner size="xl" color="blue.500" />
<Text color="gray.500">Loading documentation...</Text>
</VStack>
) : docContent ? (
<Box
sx={{
'& pre': {
borderRadius: 'md',
marginBottom: '1rem',
},
'& table': {
width: '100%',
marginBottom: '1rem',
borderCollapse: 'collapse',
},
'& th': {
background: useColorModeValue('gray.100', 'gray.700'),
padding: '12px',
textAlign: 'left',
borderBottom: '2px solid',
borderColor: borderColor,
},
'& td': {
padding: '12px',
borderBottom: '1px solid',
borderColor: borderColor,
},
'& hr': {
margin: '2rem 0',
borderColor: borderColor,
},
'& blockquote': {
borderLeft: '4px solid',
borderColor: 'blue.400',
paddingLeft: '1rem',
marginLeft: 0,
fontStyle: 'italic',
color: 'gray.600',
},
'& img': {
maxWidth: '100%',
borderRadius: 'md',
boxShadow: 'md',
marginBottom: '1rem',
},
}}
>
<ReactMarkdown components={markdownComponents}>
{docContent}
</ReactMarkdown>
</Box>
) : (
<VStack spacing={4} py={12}>
<Icon as={FiBook} boxSize={16} color="gray.300" />
<Text color="gray.500">Select a document to view</Text>
</VStack>
)}
</Box>
</HStack>
</Container>
</Box>
);
};
export default DevDocsPage;
+10 -17
View File
@@ -25,7 +25,8 @@ import {
useColorModeValue,
} from '@chakra-ui/react';
import { RefreshCw, ExternalLink, Calendar, Image as ImageIcon, Eye } from 'lucide-react';
import AdminLayout from '../../components/layout/AdminLayout';
import AdminLayout from '../../layouts/AdminLayout';
import api from '../../services/api';
interface Album {
id: string;
@@ -114,20 +115,8 @@ const GalleryAdminPage: React.FC = () => {
setRefreshing(true);
try {
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1';
const token = localStorage.getItem('token');
const response = await fetch(`${apiUrl}/admin/gallery/refresh`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Chyba při obnově galerie');
}
// Use the api service which automatically includes authentication
await api.post('/admin/gallery/refresh');
toast({
title: 'Galerie obnovena',
@@ -140,13 +129,17 @@ const GalleryAdminPage: React.FC = () => {
// Reload albums after refresh
await fetchAlbums();
} catch (err: any) {
const errorMessage = err.response?.data?.error || err.message || 'Nepodařilo se obnovit galerii';
toast({
title: 'Chyba',
description: err.message || 'Nepodařilo se obnovit galerii',
title: 'Chyba při obnově galerie',
description: errorMessage,
status: 'error',
duration: 5000,
isClosable: true,
});
console.error('Gallery refresh error:', err);
} finally {
setRefreshing(false);
}
@@ -562,9 +562,9 @@ const MatchesAdminPage = () => {
return (
<AdminLayout requireAdmin={false}>
<Box>
<Box bg={headerBg} color={headerText} borderRadius="xl" p={6} mb={6} boxShadow="lg">
<Box mb={6}>
<Heading size="lg" mb={2}>Správa zápasů</Heading>
<Text opacity={0.9}>
<Text color={useColorModeValue('gray.600', 'gray.400')}>
Správa a úprava zápasů. Můžete upravovat informace o zápasech, včetně názvů týmů, termínů, log a dalších detailů.
</Text>
</Box>
@@ -719,7 +719,7 @@ const NavigationAdminPage = () => {
<Box flex="1">
<HStack spacing={4}>
<Text fontSize="sm">
<strong>Načteno:</strong> {navItems.length} webových, {adminNavItems.length} admin, {socialLinks.length} sociálních
<strong>Načteno:</strong> {navItems.length} webových, {adminNavItems.length} admin
</Text>
</HStack>
</Box>
@@ -731,8 +731,7 @@ const NavigationAdminPage = () => {
<Text fontWeight="bold">Oddělená správa navigace</Text>
<Text fontSize="sm" mt={1}>
<strong>Webová navigace:</strong> Menu na veřejném webu<br/>
<strong>Admin panel:</strong> Postranní menu v administraci<br/>
<strong>Sociální sítě:</strong> Odkazy na sociální média
<strong>Admin panel:</strong> Postranní menu v administraci
</Text>
</Box>
</Alert>
@@ -741,7 +740,6 @@ const NavigationAdminPage = () => {
<TabList>
<Tab>Webová navigace</Tab>
<Tab>Admin panel</Tab>
<Tab>Sociální sítě</Tab>
</TabList>
<TabPanels>
@@ -874,100 +872,6 @@ const NavigationAdminPage = () => {
</VStack>
</VStack>
</TabPanel>
{/* Social Links Tab */}
<TabPanel>
<VStack spacing={4} align="stretch">
<Button leftIcon={<AddIcon />} colorScheme="blue" onClick={() => openSocialModal()}>
Přidat sociální síť
</Button>
{socialLinks.length === 0 ? (
<Alert status="warning">
<AlertIcon />
<Box>
<Text fontWeight="bold">Žádné sociální sítě</Text>
<Text fontSize="sm" mt={1}>
Nebyly nalezeny žádné odkazy na sociální sítě. Klikněte na "Přidat sociální síť" pro vytvoření odkazu.
</Text>
</Box>
</Alert>
) : (
<Box borderWidth="1px" borderRadius="lg" overflow="hidden">
<Table variant="simple">
<Thead>
<Tr>
<Th width="100px">Pořadí</Th>
<Th>Ikona</Th>
<Th>Platforma</Th>
<Th>URL</Th>
<Th>Viditelné</Th>
<Th>Akce</Th>
</Tr>
</Thead>
<Tbody>
{socialLinks.map((link, index) => {
const IconComponent = getSocialIcon(link.platform);
return (
<Tr key={link.id} bg={link.visible ? 'transparent' : 'gray.50'}>
<Td>
<HStack spacing={1}>
<IconButton
aria-label="Nahoru"
icon={<ChevronUpIcon />}
size="sm"
isDisabled={index === 0}
onClick={() => moveSocialLink(index, 'up')}
/>
<IconButton
aria-label="Dolů"
icon={<ChevronDownIcon />}
size="sm"
isDisabled={index === socialLinks.length - 1}
onClick={() => moveSocialLink(index, 'down')}
/>
</HStack>
</Td>
<Td>
<IconComponent size={24} />
</Td>
<Td fontWeight="bold">{link.platform}</Td>
<Td>
<Text fontSize="sm" color="gray.600" isTruncated maxW="300px">
{link.url}
</Text>
</Td>
<Td>
<Badge colorScheme={link.visible ? 'green' : 'gray'}>
{link.visible ? 'Ano' : 'Ne'}
</Badge>
</Td>
<Td>
<HStack spacing={2}>
<IconButton
aria-label="Upravit"
icon={<EditIcon />}
size="sm"
onClick={() => openSocialModal(link)}
/>
<IconButton
aria-label="Smazat"
icon={<DeleteIcon />}
size="sm"
colorScheme="red"
onClick={() => deleteSocial(link.id!)}
/>
</HStack>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</Box>
)}
</VStack>
</TabPanel>
</TabPanels>
</Tabs>
</VStack>
@@ -1184,61 +1088,6 @@ const NavigationAdminPage = () => {
</ModalFooter>
</ModalContent>
</Modal>
{/* Social Link Modal */}
<Modal isOpen={isSocialModalOpen} onClose={onSocialModalClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>{editingSocial?.id ? 'Upravit odkaz' : 'Nový odkaz'}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
<FormControl isRequired>
<FormLabel>Platforma</FormLabel>
<Select
value={editingSocial?.platform || 'facebook'}
onChange={(e) =>
setEditingSocial({ ...editingSocial!, platform: e.target.value })
}
>
{SOCIAL_PLATFORMS.map((platform) => (
<option key={platform.value} value={platform.value}>
{platform.label}
</option>
))}
</Select>
</FormControl>
<FormControl isRequired>
<FormLabel>URL</FormLabel>
<Input
value={editingSocial?.url || ''}
onChange={(e) => setEditingSocial({ ...editingSocial!, url: e.target.value })}
placeholder="https://www.facebook.com/..."
/>
</FormControl>
<FormControl display="flex" alignItems="center">
<FormLabel mb="0">Viditelné</FormLabel>
<Switch
isChecked={editingSocial?.visible ?? true}
onChange={(e) =>
setEditingSocial({ ...editingSocial!, visible: e.target.checked })
}
/>
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onSocialModalClose}>
Zrušit
</Button>
<Button colorScheme="blue" onClick={saveSocialLink}>
Uložit
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Container>
</AdminLayout>
);