import { useEffect, useRef, useState } from 'react'; import { FiSave, FiEye, FiCode, FiLayout, FiZap, FiTrash } from 'react-icons/fi'; import { Box, Heading, Text, SimpleGrid, FormControl, FormLabel, FormHelperText, Input, Select, Button, useToast, VStack, HStack, Divider, Image, Textarea, Tabs, TabList, TabPanels, Tab, TabPanel, Switch, Badge, } from '@chakra-ui/react'; import AdminLayout from '../../layouts/AdminLayout'; import { AdminSettings, getAdminSettings, updateAdminSettings } from '../../services/settings'; import { sendNewsletterTestAdvanced } from '../../services/admin/newsletter'; import { SeoSettings, getSeoSettings, updateSeoSettings } from '../../services/seo'; import { uploadFile } from '../../services/articles'; import { assetUrl } from '../../utils/url'; import { triggerPrefetch } from '../../services/admin/prefetch'; import api from '../../services/api'; const SettingsAdminPage: React.FC = () => { const toast = useToast(); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [settings, setSettings] = useState({}); const [testEmail, setTestEmail] = useState(''); const [testing, setTesting] = useState(false); const fileInputRef = useRef(null); const [seo, setSeo] = useState({}); // Analytics state const [umamiWebsiteId, setUmamiWebsiteId] = useState(''); const [umamiDomain, setUmamiDomain] = useState(''); const [umamiName, setUmamiName] = useState(''); const [umamiInitializing, setUmamiInitializing] = useState(false); const [umamiConfig, setUmamiConfig] = useState(null); useEffect(() => { setLoading(true); getAdminSettings() .then((data) => { const s = data || {}; setSettings(s); }) .catch(() => { toast({ title: 'Chyba', description: 'Nepodařilo se načíst nastavení', status: 'error' }); }) .finally(() => setLoading(false)); // load SEO getSeoSettings() .then((data) => setSeo(data || {})) .catch(() => {}) .finally(() => {}); // load Umami config fetchUmamiConfig(); }, [toast]); const fetchUmamiConfig = async () => { try { const response = await api.get('/insights/config'); setUmamiConfig(response.data); if (response.data?.website_id) { setUmamiWebsiteId(response.data.website_id); } // Set default domain from current host if (!umamiDomain) { const hostname = window.location.hostname; setUmamiDomain(hostname); } // Set default name from club name if available if (!umamiName && settings.club_name) { setUmamiName(settings.club_name); } } catch (error) { console.error('Failed to fetch Umami config:', error); } }; // Autofill SEO fields from general settings if missing useEffect(() => { setSeo((prev) => { const next: SeoSettings = { ...prev }; let changed = false; if (!next.site_title && settings.club_name) { next.site_title = settings.club_name; changed = true; } if (!next.default_og_image_url && settings.club_logo_url) { next.default_og_image_url = settings.club_logo_url; changed = true; } return changed ? next : prev; }); }, [settings.club_name, settings.club_logo_url]); const handleChange = (key: keyof AdminSettings) => (e: React.ChangeEvent) => { setSettings((prev) => ({ ...prev, [key]: e.target.value })); }; const handleSeoChange = (key: keyof SeoSettings) => (e: React.ChangeEvent) => { const v = e.target.type === 'checkbox' ? (e.target as any).checked : e.target.value; setSeo((prev) => ({ ...prev, [key]: v })); }; const sendSmtpTest = async () => { setTesting(true); try { const payload: any = { type: 'newsletter' }; if (testEmail && testEmail.trim()) payload.email = testEmail.trim(); await sendNewsletterTestAdvanced(payload); toast({ title: 'Test odeslán', description: testEmail ? `Zkontrolujte schránku ${testEmail}` : 'Test odeslán na výchozí administrátorský e‑mail.', status: 'success', duration: 5000 }); } catch (e: any) { const errorMsg = e?.response?.data?.error || e?.message || 'Neznámá chyba'; const isAuthError = errorMsg.includes('535') || errorMsg.toLowerCase().includes('authentication failed'); toast({ title: isAuthError ? '⚠️ Chyba autentizace SMTP (535)' : 'Chyba při odeslání testu', description: isAuthError ? 'Zkontrolujte SMTP heslo. Ujistěte se, že je správně zkopírované bez mezer. Některé poskytovatele vyžadují aplikační heslo místo běžného hesla.' : errorMsg + ' Zkuste upravit SMTP nastavení.', status: 'error', duration: 10000, isClosable: true, }); } finally { setTesting(false); } }; // Numeric and boolean helpers for admin settings const handleNumChange = (key: keyof AdminSettings) => (e: React.ChangeEvent) => { const val = e.target.value; const n = val === '' ? undefined : Number(val); setSettings((prev) => ({ ...prev, [key]: (Number.isFinite(n as number) ? (n as any) : undefined) })); }; const handleBoolChange = (key: keyof AdminSettings) => (e: React.ChangeEvent) => { const checked = (e.target as any).checked as boolean; setSettings((prev) => ({ ...prev, [key]: checked as any })); }; const saveSEO = async () => { try { await updateSeoSettings(seo); toast({ title: 'SEO uloženo', status: 'success' }); } catch (e: any) { toast({ title: 'Chyba', description: e?.message || 'Uložení SEO nastavení se nezdařilo', status: 'error' }); } }; const handleSelectChange = (key: keyof AdminSettings) => (e: React.ChangeEvent) => { setSettings((prev) => ({ ...prev, [key]: e.target.value })); }; const presetStorageThresholds = () => { setSettings((prev) => ({ ...prev, storage_warn_threshold: 80 as any, storage_critical_threshold: 95 as any, })); }; const handleSave = async () => { setSaving(true); try { const payload: Partial = { club_name: settings.club_name, club_logo_url: settings.club_logo_url, // social & gallery links facebook_url: (settings as any).facebook_url, instagram_url: (settings as any).instagram_url, youtube_url: (settings as any).youtube_url, // generic gallery (preferred) gallery_url: (settings as any).gallery_url, // backward compatibility zonerama_url: (settings as any).zonerama_url, // SMTP smtp_host: (settings as any).smtp_host, smtp_port: (settings as any).smtp_port as any, smtp_user: (settings as any).smtp_user, smtp_password: (settings as any).smtp_password, smtp_from: (settings as any).smtp_from, smtp_from_name: (settings as any).smtp_from_name, smtp_encryption: (settings as any).smtp_encryption as any, ...(typeof (settings as any).smtp_auth === 'boolean' ? { smtp_auth: (settings as any).smtp_auth as any } : {}), smtp_skip_verify: (settings as any).smtp_skip_verify as any, // videos module videos_module_enabled: (settings as any).videos_module_enabled as any, videos_source: (settings as any).videos_source as any, // contact & map contact_address: (settings as any).contact_address, contact_city: (settings as any).contact_city, contact_zip: (settings as any).contact_zip, contact_country: (settings as any).contact_country, contact_phone: (settings as any).contact_phone, contact_email: (settings as any).contact_email, location_latitude: (settings as any).location_latitude as any, location_longitude: (settings as any).location_longitude as any, map_zoom_level: (settings as any).map_zoom_level as any, show_map_on_homepage: (typeof (settings as any).location_latitude === 'number') && (typeof (settings as any).location_longitude === 'number'), map_style: (settings as any).map_style, frontend_base_url: (settings as any).frontend_base_url, api_base_url: (settings as any).api_base_url, // homepage matches display finished_match_display_days: (settings as any).finished_match_display_days as any, storage_warn_threshold: (settings as any).storage_warn_threshold as any, storage_critical_threshold: (settings as any).storage_critical_threshold as any, // error-review integration (domain managed via .env; only tokens are saved) error_review_admin_token: (settings as any).error_review_admin_token, error_review_ingest_token: (settings as any).error_review_ingest_token, }; const saved = await updateAdminSettings(payload); setSettings((prev) => ({ ...prev, ...saved })); toast({ title: 'Uloženo', description: 'Nastavení bylo úspěšně aktualizováno', status: 'success' }); // Try to refresh prefetch caches try { await triggerPrefetch(); } catch {} try { const fb = String(((saved as any).frontend_base_url || (settings as any).frontend_base_url || '')).replace(/\/$/, ''); let ab = String(((saved as any).api_base_url || (settings as any).api_base_url || '')).trim(); if (fb || ab) { try { const u = new URL(ab || '', fb || (typeof window !== 'undefined' ? window.location.origin : '')); if (!/\/api\//.test(u.pathname)) { u.pathname = u.pathname.replace(/\/$/, '') + '/api/v1'; } ab = u.toString(); } catch {} try { localStorage.setItem('fc_frontend_base_url', fb); } catch {} try { localStorage.setItem('fc_api_base_url', ab); } catch {} try { localStorage.setItem('api_base_url', ab); } catch {} try { (api as any).defaults.baseURL = ab; } catch {} setTimeout(() => { try { window.location.reload(); } catch {} }, 600); } // Persist ingest token for frontend errorReporter (URL is fixed by default wiring) try { const ingestToken = (saved as any).error_review_ingest_token || (settings as any).error_review_ingest_token; if (ingestToken) localStorage.setItem('fc_error_ingest_token', String(ingestToken)); } catch {} } catch {} } catch (e: any) { toast({ title: 'Chyba', description: e?.message || 'Uložení nastavení se nezdařilo', status: 'error' }); } finally { setSaving(false); } }; const onSelectLogo = () => fileInputRef.current?.click(); const onLogoChosen: React.ChangeEventHandler = async (e) => { try { const file = e.target.files?.[0]; if (!file) return; // Basic validation: accept PNG/SVG/PDF and limit ~5MB const okType = ['image/png', 'image/svg+xml', 'application/pdf'].includes(file.type); if (!okType) { toast({ title: 'Nepodporovaný formát', description: 'Nahrajte prosím logo ve formátu PNG, SVG nebo PDF.', status: 'warning' }); return; } if (file.size > 5 * 1024 * 1024) { toast({ title: 'Soubor je příliš velký', description: 'Maximálně 5 MB.', status: 'warning' }); return; } const res = await uploadFile(file); setSettings((prev) => ({ ...prev, club_logo_url: res.url })); toast({ title: 'Logo nahráno', status: 'success' }); } catch (err: any) { toast({ title: 'Nahrávání selhalo', description: err?.message || 'Logo se nepodařilo nahrát', status: 'error' }); } finally { if (fileInputRef.current) fileInputRef.current.value = ''; } }; return ( Nastavení webu {['Obecné','Sociální sítě','Videa','SMTP','Analytika','SEO'].map((label) => ( {label} ))} Název klubu Úložiště souborů Varování při (%) Kritické při (%) Přednastavit Logo klubu {settings.club_logo_url && ( Logo )} Nastavení URL URL webu API URL Ujistěte se, že adresa končí na /api/v1 Zobrazení zápasů Počet dní zobrazení dokončených zápasů { const val = parseInt(e.target.value, 10); setSettings((prev) => ({ ...prev, finished_match_display_days: isNaN(val) ? 2 : val } as any)); }} /> Počet dní, po které se na úvodní stránce zobrazí dokončený zápas s výsledkem místo příštího zápasu (výchozí: 2 dny). Facebook URL Instagram URL YouTube URL Pro automatické načítání videí vyplňte YouTube kanál. Fotogalerie URL ℹ️ Sekce Videa {(settings as any).videos_module_enabled && ( Aktivní )} Videa spravujte na stránce{' '} . Zde nastavte zobrazení na titulní stránce. {(settings as any).youtube_url && ( Sekce se automaticky aktivuje při vyplnění YouTube kanálu v tabě Sociální sítě. )} Zobrazit sekci Videa na titulní stránce Zdroj videí Pro automatický zdroj vyplňte v tabě „Sociální sítě" pole YouTube URL. SMTP Host Adresa SMTP serveru. Příklad: smtp.seznam.cz, smtp.gmail.com, smtp.office365.com SMTP Port Nejčastěji 587 (TLS/STARTTLS) nebo 465 (SSL). Port 25 je bez šifrování a často blokovaný. SMTP Uživatel Přihlašovací jméno k SMTP (obvykle e‑mailová adresa). Nechte prázdné, pokud server nevyžaduje přihlášení. SMTP Heslo Heslo k účtu nebo aplikační heslo (Gmail/Seznam/Office 365 často vyžadují). Vkládejte bez mezer. From e‑mail Adresa odesílatele uvedená v e‑mailech. Ideálně existující schránka na vašem SMTP serveru. From jméno Zobrazované jméno odesílatele (např. FK Váš Klub). Šifrování SSL = implicitní šifrování (obvykle port 465). TLS/STARTTLS = šifrování po navázání spojení (obvykle port 587). Autentizace Mechanismus přihlášení k SMTP. Pokud si nejste jisti, zvolte PLAIN nebo LOGIN. Někteří poskytovatelé vyžadují konkrétní metodu. Přeskočit ověření certifikátu Pokročilé: povolte pouze při chybách certifikátu (self‑signed apod.). Nedoporučeno v produkci – snižuje bezpečnost. Test e‑mail setTestEmail(e.target.value)} maxW={{ base: '100%', md: '320px' }} /> Webová analytika Nastavení sledování návštěvnosti webu pomocí open-source nástroje pro webovou analytiku. {umamiConfig?.enabled && umamiConfig?.website_id ? ( ✓ Analytika je aktivní Website ID: {umamiConfig.website_id} ) : ( ⚠ Analytika není nakonfigurována Vytvořte webovou stránku pro aktivaci sledování. )} Vytvořit novou webovou stránku Název webové stránky setUmamiName(e.target.value)} placeholder={settings.club_name || 'Fotbal Club'} /> Název, který se zobrazí v dashboardu analytiky Doména setUmamiDomain(e.target.value)} placeholder={window.location.hostname} /> Doména vašeho webu (např. fotbal.example.com) Poznámka: Pro aktivaci analytiky je nutné mít nastavené přihlašovací údaje v .env souboru. SEO nastavení Základní SEO nastavení pro lepší viditelnost ve vyhledávačích. Titulek webu Hlavní název webu, zobrazí se v záložce prohlížeče a ve výsledcích vyhledávání Popis webu