mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Input,
|
||||
List,
|
||||
ListItem,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Icon,
|
||||
Box,
|
||||
Kbd,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaSearch, FaCog, FaNewspaper, FaUsers, FaImage, FaHandshake, FaEnvelope, FaAward, FaSyncAlt, FaVideo, FaCalendarAlt, FaPalette, FaCommentAlt, FaKey, FaChartLine, FaBook, FaTools, FaBell, FaBars } from 'react-icons/fa';
|
||||
|
||||
export type AdminSearchItem = {
|
||||
label: string;
|
||||
path: string;
|
||||
section: string;
|
||||
keywords?: string[];
|
||||
icon?: any;
|
||||
};
|
||||
|
||||
const adminIndex: AdminSearchItem[] = [
|
||||
{ label: 'Dashboard', path: '/admin', section: 'Core', keywords: ['overview', 'stat', 'dashboard'], icon: FaTools },
|
||||
{ label: 'Články', path: '/admin/clanky', section: 'Obsah', keywords: ['articles', 'posts', 'blog'], icon: FaNewspaper },
|
||||
{ label: 'Hráči', path: '/admin/hraci', section: 'Kádry', keywords: ['players'], icon: FaUsers },
|
||||
{ label: 'Týmy', path: '/admin/tymy', section: 'Kádry', keywords: ['teams'], icon: FaUsers },
|
||||
{ label: 'Zápasy', path: '/admin/zapasy', section: 'FAČR', keywords: ['matches', 'facr'], icon: FaCalendarAlt },
|
||||
{ label: 'Média', path: '/admin/media', section: 'Obsah', keywords: ['uploads', 'images'], icon: FaImage },
|
||||
{ label: 'Sponzoři', path: '/admin/sponzori', section: 'Marketing', keywords: ['sponsors', 'partners'], icon: FaHandshake },
|
||||
{ label: 'Bannery', path: '/admin/bannery', section: 'Marketing', keywords: ['banners'], icon: FaImage },
|
||||
{ label: 'Kategorie', path: '/admin/kategorie', section: 'Obsah', keywords: ['categories'], icon: FaAward },
|
||||
{ label: 'Nastavení', path: '/admin/nastaveni', section: 'Systém', keywords: ['settings', 'config'], icon: FaCog },
|
||||
{ label: 'Newsletter', path: '/admin/newsletter', section: 'Komunikace', keywords: ['email', 'campaign'], icon: FaEnvelope },
|
||||
{ label: 'Uživatelé', path: '/admin/uzivatele', section: 'Systém', keywords: ['users', 'accounts'], icon: FaKey },
|
||||
{ label: 'Prefetch', path: '/admin/prefetch', section: 'Systém', keywords: ['cache', 'fetch'], icon: FaSyncAlt },
|
||||
{ label: 'Galerie', path: '/admin/galerie', section: 'Média', keywords: ['gallery', 'zonerama'], icon: FaImage },
|
||||
{ label: 'Videa', path: '/admin/videa', section: 'Média', keywords: ['youtube', 'videos'], icon: FaVideo },
|
||||
{ label: 'Analytika', path: '/admin/analytika', section: 'SEO', keywords: ['analytics', 'umami'], icon: FaChartLine },
|
||||
{ label: 'O klubu', path: '/admin/o-klubu', section: 'Obsah', keywords: ['about'], icon: FaPalette },
|
||||
{ label: 'Navigace', path: '/admin/navigace', section: 'Systém', keywords: ['navigation', 'menu', 'sidebar'], icon: FaBars },
|
||||
{ label: 'Notifikace: Zápasy', path: '/admin/notifications', section: 'Komunikace', keywords: ['notifications', 'match'], icon: FaBell },
|
||||
// Docs
|
||||
{ label: 'Dokumentace (Úvod)', path: '/admin/docs#uvod', section: 'Docs', keywords: ['docs', 'documentation'], icon: FaBook },
|
||||
{ label: 'Dokumentace (Nastavení)', path: '/admin/docs#nastaveni', section: 'Docs', keywords: ['docs', 'settings'], icon: FaBook },
|
||||
{ label: 'Dokumentace (Články)', path: '/admin/docs#clanky', section: 'Docs', keywords: ['docs', 'articles'], icon: FaBook },
|
||||
{ label: 'Dokumentace (Newsletter)', path: '/admin/docs#newsletter', section: 'Docs', keywords: ['docs', 'email'], icon: FaBook },
|
||||
{ label: 'Dokumentace (Řešení problémů)', path: '/admin/docs#troubleshooting', section: 'Docs', keywords: ['docs', 'troubleshooting'], icon: FaBook },
|
||||
];
|
||||
|
||||
function highlight(text: string, q: string) {
|
||||
if (!q) return text;
|
||||
try {
|
||||
const esc = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const re = new RegExp(esc, 'gi');
|
||||
const parts = text.split(re);
|
||||
const matches = text.match(re) || [];
|
||||
const out: any[] = [];
|
||||
parts.forEach((p, idx) => {
|
||||
out.push(p);
|
||||
if (idx < matches.length) out.push(<mark key={idx} style={{ backgroundColor: '#fde68a' }}>{matches[idx]}</mark>);
|
||||
});
|
||||
return <>{out}</>;
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function score(item: AdminSearchItem, q: string) {
|
||||
const t = (item.label || '').toLowerCase();
|
||||
const b = q.toLowerCase();
|
||||
const kws = (item.keywords || []).join(' ').toLowerCase();
|
||||
let s = 0;
|
||||
if (!b) return s;
|
||||
if (t === b) s += 200;
|
||||
if (t.startsWith(b)) s += 120;
|
||||
if (t.includes(b)) s += 80 - t.indexOf(b);
|
||||
if (kws.includes(b)) s += 40;
|
||||
// small preference for Docs when # present
|
||||
if (item.section === 'Docs' && item.path.includes('#')) s += 5;
|
||||
return s;
|
||||
}
|
||||
|
||||
export default function AdminSearchModal({ isOpen, onClose, onSelectPath }: { isOpen: boolean; onClose: () => void; onSelectPath: (path: string) => void }) {
|
||||
const [q, setQ] = useState('');
|
||||
const [debounced, setDebounced] = useState('');
|
||||
const [idx, setIdx] = useState(-1);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => setDebounced(q.trim()), 250);
|
||||
return () => clearTimeout(id);
|
||||
}, [q]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setQ('');
|
||||
setDebounced('');
|
||||
setIdx(-1);
|
||||
setTimeout(() => inputRef.current?.focus(), 50);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const results = useMemo(() => {
|
||||
const arr = adminIndex.map((it) => ({ it, s: score(it, debounced) }))
|
||||
.filter((r) => r.s > 0 || !debounced)
|
||||
.sort((a, b) => b.s - a.s || a.it.label.localeCompare(b.it.label))
|
||||
.slice(0, 12)
|
||||
.map((r) => r.it);
|
||||
return arr;
|
||||
}, [debounced]);
|
||||
|
||||
const onSelect = useCallback((path: string) => {
|
||||
onClose();
|
||||
onSelectPath(path);
|
||||
}, [onClose, onSelectPath]);
|
||||
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||
const n = results.length;
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setIdx((i) => Math.min(n - 1, i + 1)); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setIdx((i) => Math.max(-1, i - 1)); }
|
||||
else if (e.key === 'Enter') {
|
||||
const chosen = idx >= 0 ? results[idx] : results[0];
|
||||
if (chosen) onSelect(chosen.path);
|
||||
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
|
||||
e.preventDefault(); onClose();
|
||||
} else if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg" motionPreset="scale">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
Admin vyhledávání
|
||||
<Box as="span" ml={3} color="gray.500" fontSize="sm">
|
||||
<Kbd>Ctrl</Kbd>+<Kbd>K</Kbd>
|
||||
</Box>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={4}>
|
||||
<InputGroup size="lg">
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<Icon as={FaSearch} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="Hledat v administraci (stránky, nastavení, dokumentace)"
|
||||
value={q}
|
||||
onChange={(e) => { setQ(e.target.value); setIdx(-1); }}
|
||||
onKeyDown={onKeyDown}
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<List mt={4} spacing={1}>
|
||||
{results.map((r, i) => (
|
||||
<ListItem
|
||||
key={r.path}
|
||||
px={3}
|
||||
py={2}
|
||||
borderRadius="md"
|
||||
cursor="pointer"
|
||||
bg={i === idx ? 'blackAlpha.50' : 'transparent'}
|
||||
_hover={{ bg: 'blackAlpha.50' }}
|
||||
onClick={() => onSelect(r.path)}
|
||||
>
|
||||
<HStack>
|
||||
{r.icon ? <Icon as={r.icon} color="blue.500" /> : null}
|
||||
<Text fontWeight="semibold">{highlight(r.label, debounced)}</Text>
|
||||
<Badge ml="auto" colorScheme="gray">{r.section}</Badge>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
))}
|
||||
{results.length === 0 && (
|
||||
<Box color="gray.500" fontSize="sm" px={1} py={2}>Žádné výsledky</Box>
|
||||
)}
|
||||
</List>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user