This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
@@ -0,0 +1,404 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Box, Heading, Text, List, ListItem, Link, Divider, Code, OrderedList, HStack, IconButton, useColorModeValue, useToast } from '@chakra-ui/react';
import { FaLink, FaArrowUp } from 'react-icons/fa';
import AdminLayout from '../../layouts/AdminLayout';
const AdminDocsPage: React.FC = () => {
const sections = useMemo(() => [
{ id: 'nastaveni', label: 'Nastavení (branding + SMTP)' },
{ id: 'clanky', label: 'Články a kategorie' },
{ id: 'zapasy', label: 'Zápasy (FAČR)' },
{ id: 'hraci-tymy', label: 'Hráči a týmy' },
{ id: 'media', label: 'Média' },
{ id: 'sponzori-bannery', label: 'Sponzoři a bannery' },
{ id: 'newsletter', label: 'Newsletter' },
{ id: 'aliasy', label: 'Alias soutěží' },
{ id: 'prefetch', label: 'Prefetch' },
{ id: 'videa', label: 'Videa' },
{ id: 'aktivity', label: 'Aktivity' },
{ id: 'merch', label: 'Oblečení' },
{ id: 'zpravy', label: 'Zprávy' },
{ id: 'reset-admin', label: 'Reset hesla' },
{ id: 'uzivatele', label: 'Uživatelé' },
{ id: 'seo-analytics', label: 'SEO a Analytics' },
{ id: 'troubleshooting', label: 'Troubleshooting' },
], []);
const [activeId, setActiveId] = useState<string>('');
const toast = useToast();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
// Pick the entry closest to the top that is intersecting
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => (a.boundingClientRect.top > b.boundingClientRect.top ? 1 : -1));
if (visible[0]) {
setActiveId(visible[0].target.id);
}
},
{ rootMargin: '-40% 0px -50% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] }
);
const els = sections.map((s) => document.getElementById(s.id)).filter(Boolean) as Element[];
els.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, [sections]);
// Persist active section and restore on reload
useEffect(() => {
if (activeId) {
try { localStorage.setItem('adminDocs:lastAnchor', activeId); } catch {}
}
}, [activeId]);
useEffect(() => {
// Run after paint so layout is ready
const t = setTimeout(() => {
const hash = (window.location.hash || '').replace('#', '').trim();
let targetId = hash;
if (!targetId) {
try { targetId = localStorage.getItem('adminDocs:lastAnchor') || ''; } catch {}
}
if (targetId) {
const el = document.getElementById(targetId);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
setActiveId(targetId);
}
}
}, 50);
return () => clearTimeout(t);
}, []);
const copyDeepLink = async (id: string) => {
try {
const url = `${window.location.origin}${window.location.pathname}#${id}`;
await navigator.clipboard.writeText(url);
toast({ title: 'Odkaz zkopírován', status: 'success', duration: 1500, isClosable: true });
} catch {
toast({ title: 'Nelze zkopírovat odkaz', status: 'error', duration: 2000, isClosable: true });
}
};
const tocActiveColor = useColorModeValue('blue.700', 'blue.300');
const tocActiveBg = useColorModeValue('blue.50', 'blue.900');
return (
<AdminLayout>
<Box
display={{ base: 'block', md: 'grid' }}
gridTemplateColumns={{ base: '1fr', md: '260px 1fr' }}
gap={{ base: 0, md: 6 }}
>
{/* Sticky TOC */}
<Box display={{ base: 'none', md: 'block' }}>
<Box position="sticky" top="84px">
<Box bg="white" p={4} borderRadius="lg" borderWidth="1px" boxShadow="sm">
<Heading size="sm" mb={3}>Rychlá navigace</Heading>
<OrderedList spacing={2} pl={5}>
{sections.map((s) => (
<ListItem key={s.id}>
<Link
href={`#${s.id}`}
display="block"
px={2}
py={1}
borderRadius="md"
bg={activeId === s.id ? tocActiveBg : 'transparent'}
color={activeId === s.id ? tocActiveColor : 'inherit'}
onClick={() => {
try { localStorage.setItem('adminDocs:lastAnchor', s.id); } catch {}
}}
>
{s.label}
</Link>
</ListItem>
))}
</OrderedList>
</Box>
</Box>
</Box>
{/* Main content */}
<Box bg="white" p={6} borderRadius="lg" borderWidth="1px" boxShadow="sm">
<Box id="top" />
<Heading size="lg" mb={1}>Dokumentace</Heading>
<Text color="gray.600" mb={4}>Přehled funkcí, postupů a řešení problémů</Text>
<Heading size="md" mb={3}>Začínáme</Heading>
<Text mb={4}>
Tato stránka shrnuje hlavní možnosti administrace webu. Pro rychlý start doporučujeme nejprve vyplnit
<Link href="/admin/nastaveni" ml={1} color="blue.600">Nastavení</Link>, poté vytvořit první článek
a ověřit funkčnost emailů.
</Text>
<Heading size="sm" mt={6} mb={2}>Obsah</Heading>
<OrderedList spacing={1} mb={4} pl={5}>
<ListItem><Link href="#nastaveni">Nastavení klubu, branding a SMTP</Link></ListItem>
<ListItem><Link href="#clanky">Články a kategorie</Link></ListItem>
<ListItem><Link href="#zapasy">Zápasy a výsledky (FAČR)</Link></ListItem>
<ListItem><Link href="#hraci-tymy">Hráči a týmy</Link></ListItem>
<ListItem><Link href="#media">Média (soubory a obrázky)</Link></ListItem>
<ListItem><Link href="#sponzori-bannery">Sponzoři a bannery</Link></ListItem>
<ListItem><Link href="#newsletter">Newsletter</Link></ListItem>
<ListItem><Link href="#aliasy">Alias názvů soutěží</Link></ListItem>
<ListItem><Link href="#prefetch">Prefetch (YouTube a další data)</Link></ListItem>
<ListItem><Link href="#uzivatele">Uživatelé a přístupy</Link></ListItem>
<ListItem><Link href="#seo-analytics">SEO a Analytics</Link></ListItem>
<ListItem><Link href="#troubleshooting">Troubleshooting (řešení problémů)</Link></ListItem>
</OrderedList>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="nastaveni" size="md">Nastavení klubu, branding a SMTP</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('nastaveni')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem><strong>Branding</strong>: Nastavte název klubu, logo a barvy. Tyto hodnoty se propsají do emailů i vzhledu webu.</ListItem>
<ListItem><strong>SMTP</strong>: Vyplňte host, port, uživatele a heslo. U portu 465 se používá implicitní SSL, jinak STARTTLS.</ListItem>
<ListItem><strong>Test</strong>: V sekci Newsletter Test odešlete zkušební email a ověřte doručení.</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="clanky" size="md">Články a kategorie</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('clanky')} />
</HStack>
<OrderedList spacing={2} pl={5}>
<ListItem>Vytvořte kategorii (pokud ještě neexistuje) a poté nový článek.</ListItem>
<ListItem>Vyplňte SEO název a popis zlepší to výsledky ve vyhledávání.</ListItem>
<ListItem>Obrázek nahrávejte do Média a vložte URL do článku.</ListItem>
</OrderedList>
<Text mt={3} fontSize="sm" color="gray.600">
Další správa kategorií je v sekci <Link href="/admin/kategorie">Kategorie</Link>.
</Text>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="zapasy" size="md">Zápasy a výsledky (FAČR)</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('zapasy')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Napojení na FAČR umožňuje zobrazit soutěže, tabulky a nadcházející utkání.</ListItem>
<ListItem>Přes <Link href="/admin/aliasy-soutezi">Alias názvů soutěží</Link> si můžete upravit názvy pro frontend.</ListItem>
<ListItem>Pokud FAČR data nevidíte, spusťte <Link href="/admin/prefetch">Prefetch</Link> pro načtení cache.</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="hraci-tymy" size="md">Hráči a týmy</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('hraci-tymy')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Přidávejte týmy a hráče, udržujte je aktuální pro přehled na webu.</ListItem>
<ListItem>Loga týmů lze přepsat v sekci <Link href="/admin/aliasy-soutezi">Alias/Overrides</Link>, pokud se nenačtou správně.</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="media" size="md">Média (soubory a obrázky)</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('media')} />
</HStack>
<OrderedList spacing={2} pl={5}>
<ListItem>Nahrajte soubor v sekci Média, zkopírujte URL a použijte ji v článcích nebo bannerech.</ListItem>
<ListItem>Podporované formáty: JPEG/PNG/SVG/ dle konfigurace serveru.</ListItem>
</OrderedList>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="sponzori-bannery" size="md">Sponzoři a bannery</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('sponzori-bannery')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Přidávejte sponzory s logem a odkazem. Můžete je zobrazit na vybraných sekcích webu.</ListItem>
<ListItem>Pro reklamní plochy používejte sekci Bannery a nastavte cílové URL.</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="newsletter" size="md">Newsletter</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('newsletter')} />
</HStack>
<OrderedList spacing={2} pl={5}>
<ListItem>Nastavte SMTP a identitu odesílatele (jméno + adresa).</ListItem>
<ListItem>Odešlete test na svou adresu, ověřte zobrazení a doručení.</ListItem>
<ListItem>Hromadné rozesílky plánujte s ohledem na limity poskytovatele SMTP.</ListItem>
</OrderedList>
<Heading size="sm" mt={4} mb={2}>Postup: konfigurace SMTP (krok za krokem)</Heading>
<OrderedList spacing={1} pl={5}>
<ListItem>Otevřete <Link href="/admin/nastaveni">Nastavení</Link> a vyplňte Host, Port, Uživatelské jméno, Heslo a From adresu.</ListItem>
<ListItem>Pro port <Code>465</Code> použijte SSL. Pro port <Code>587</Code> použijte STARTTLS (Use TLS ano).</ListItem>
<ListItem>Uložte změny.</ListItem>
<ListItem>Přejděte do <Link href="/admin/newsletter">Newsletter</Link> a odešlete testovací email na vaši adresu.</ListItem>
<ListItem>Zkontrolujte doručení v inboxu a případně složku spam. Pokud nedorazí, viz Troubleshooting níže.</ListItem>
</OrderedList>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="aliasy" size="md">Alias názvů soutěží</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('aliasy')} />
</HStack>
<Text mb={2}>Upravit zobrazení soutěží (přejmenování, sjednocení názvů) můžete v sekci Alias. Změny se projeví na frontendu.</Text>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="prefetch" size="md">Prefetch (YouTube a další data)</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('prefetch')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Prefetch vytváří rychlou cache pro veřejné části webu (YouTube, články).</ListItem>
<ListItem>Při problémech s rychlostí načtení spusťte ručně Prefetch a zkontrolujte stav.</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="videa" size="md">Videa</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('videa')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Správa videí a playlistů zobrazovaných na webu.</ListItem>
<ListItem>Pro rychlé načítání videí použijte <Link href="/admin/prefetch">Prefetch</Link> (YouTube cache).</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="aktivity" size="md">Aktivity</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('aktivity')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Plánujte a publikujte klubové akce mimo soutěžní zápasy.</ListItem>
<ListItem>Aktivity se mohou zobrazovat na nástěnce administrace i na veřejném webu.</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="merch" size="md">Oblečení (Merch)</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('merch')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Správa položek klubového merche a jejich zobrazení pro fanoušky.</ListItem>
<ListItem>Pro každou položku nastavte název, popis, cenu a obrázek (z Média).</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="zpravy" size="md">Zprávy</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('zpravy')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Seznam zpráv odeslaných přes kontaktní formulář a systémové notifikace.</ListItem>
<ListItem>Odpovídejte uživatelům přímo z vaší schránky, systém ukládá pouze záznamy.</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="reset-admin" size="md">Reset hesla (admin nástroj)</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('reset-admin')} />
</HStack>
<OrderedList spacing={2} pl={5}>
<ListItem>Přejděte na <Link href="/admin/users/send-reset">Nástroj pro reset hesla</Link>.</ListItem>
<ListItem>Zadejte email uživatele a odešlete ověřovací kód/odkaz pro reset.</ListItem>
<ListItem>Uživatel dokončí změnu hesla přes veřejnou stránku pro reset.</ListItem>
</OrderedList>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="uzivatele" size="md">Uživatelé a přístupy</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('uzivatele')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Rozlišujeme role <Code>admin</Code> a <Code>user</Code>. Admin přístup do všech sekcí.</ListItem>
<ListItem>Hesla mají minimální délku 8 znaků a nesmí obsahovat mezery.</ListItem>
<ListItem>Zapomenuté heslo lze obnovit přes stránku Zapomenuté heslo (ověřovací kód emailem).</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="seo-analytics" size="md">SEO a Analytics</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('seo-analytics')} />
</HStack>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>U článků vyplňujte <strong>SEO titulek</strong> a <strong>SEO popis</strong>.</ListItem>
<ListItem>V administraci je k dispozici přehled návštěvnosti a interakcí (sekce Analytics).</ListItem>
</List>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
<Divider my={6} />
<HStack align="center" justify="space-between" mb={2}>
<Heading id="troubleshooting" size="md">Troubleshooting (řešení problémů)</Heading>
<IconButton aria-label="Zkopírovat odkaz" variant="ghost" size="sm" icon={<FaLink />} onClick={() => copyDeepLink('troubleshooting')} />
</HStack>
<Heading size="sm" mb={2}>Emaily se neodesílají</Heading>
<List spacing={2} styleType="disc" pl={5} mb={4}>
<ListItem>Zkontrolujte <Link href="/admin/nastaveni">SMTP nastavení</Link> (host, port, uživatel, heslo, šifrování).</ListItem>
<ListItem>Pro port 465 použijte SSL, pro 587 STARTTLS. Využijte tlačítko Otestovat SMTP v průvodci či v newsletteru.</ListItem>
<ListItem>Mrkněte do logů serveru na případné chyby SMTP.</ListItem>
</List>
<Heading size="sm" mb={2}>Obrázky/Logo se nezobrazuje</Heading>
<List spacing={2} styleType="disc" pl={5} mb={4}>
<ListItem>Ověřte, že URL je správná a veřejně dostupná. V případě externích zdrojů lze využít proxy `/api/v1/proxy/image`.</ListItem>
<ListItem>Nahrajte logo do sekce Média a použijte relativní cestu (např. <Code>/uploads/2025/01/logo.png</Code>).</ListItem>
</List>
<Heading size="sm" mb={2}>FAČR data nejsou aktuální</Heading>
<List spacing={2} styleType="disc" pl={5} mb={4}>
<ListItem>Spusťte <Link href="/admin/prefetch">Prefetch</Link> a porovnejte čas posledního běhu.</ListItem>
<ListItem>Zkontrolujte internetové připojení serveru a limity volání.</ListItem>
</List>
<Heading size="sm" mb={2}>Přihlášení nefunguje</Heading>
<List spacing={2} styleType="disc" pl={5} mb={4}>
<ListItem>Ověřte email/heslo a zda účet roli <Code>admin</Code>.</ListItem>
<ListItem>Pokud jste zapomněli heslo, použijte Zapomenuté heslo a kód z emailu.</ListItem>
</List>
<Heading size="sm" mb={2}>Newsletter padá do spamu</Heading>
<List spacing={2} styleType="disc" pl={5}>
<ListItem>Nastavte správně DNS záznamy <Code>SPF</Code>, <Code>DKIM</Code> a pokud možno <Code>DMARC</Code>.</ListItem>
<ListItem>Ověřte, že From doména odpovídá doméně vašeho SMTP odesílatele.</ListItem>
</List>
<Divider my={6} />
<Heading size="md" mb={3}>API základ</Heading>
<Text mb={2}>Veřejná API základna: <Code>/api/v1</Code></Text>
<Text fontSize="sm" color="gray.600">(Rozšířená dokumentace API bude doplněna v budoucnu.)</Text>
<Box mt={2}><Link href="#top" color="blue.600"><HStack as="span" spacing={2}><FaArrowUp /><Text>Na začátek</Text></HStack></Link></Box>
</Box>
</Box>
</AdminLayout>
);
};
export default AdminDocsPage;