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