mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
766 lines
35 KiB
TypeScript
766 lines
35 KiB
TypeScript
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<boolean>(false);
|
||
const [saving, setSaving] = useState<boolean>(false);
|
||
const [settings, setSettings] = useState<AdminSettings>({});
|
||
const [testEmail, setTestEmail] = useState<string>('');
|
||
const [testing, setTesting] = useState<boolean>(false);
|
||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||
const [seo, setSeo] = useState<SeoSettings>({});
|
||
// Analytics state
|
||
const [umamiWebsiteId, setUmamiWebsiteId] = useState<string>('');
|
||
const [umamiDomain, setUmamiDomain] = useState<string>('');
|
||
const [umamiName, setUmamiName] = useState<string>('');
|
||
const [umamiInitializing, setUmamiInitializing] = useState<boolean>(false);
|
||
const [umamiConfig, setUmamiConfig] = useState<any>(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<HTMLInputElement>) => {
|
||
setSettings((prev) => ({ ...prev, [key]: e.target.value }));
|
||
};
|
||
|
||
const handleSeoChange = (key: keyof SeoSettings) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
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<HTMLInputElement>) => {
|
||
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<HTMLInputElement>) => {
|
||
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<HTMLSelectElement>) => {
|
||
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<AdminSettings> = {
|
||
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<HTMLInputElement> = 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 (
|
||
<AdminLayout>
|
||
<Box>
|
||
<Heading size="lg" mb={4}>Nastavení webu</Heading>
|
||
|
||
<Tabs variant="enclosed" colorScheme="brand">
|
||
<TabList flexWrap="wrap">
|
||
{['Obecné','Sociální sítě','Videa','SMTP','Analytika','SEO'].map((label) => (
|
||
<Tab key={label}
|
||
_selected={{ bg: 'brand.primary', color: 'text.onPrimary', borderColor: 'brand.primary' }}
|
||
_hover={{ bg: 'rgba(0,0,0,0.04)', borderColor: 'brand.500' }}
|
||
>{label}</Tab>
|
||
))}
|
||
</TabList>
|
||
<TabPanels>
|
||
<TabPanel>
|
||
<VStack align="stretch" spacing={4}>
|
||
<FormControl>
|
||
<FormLabel>Název klubu</FormLabel>
|
||
<Input value={settings.club_name || ''} onChange={handleChange('club_name')} />
|
||
</FormControl>
|
||
|
||
<Heading size="sm">Úložiště souborů</Heading>
|
||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={4}>
|
||
<FormControl>
|
||
<FormLabel>Varování při (%)</FormLabel>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
value={(settings as any).storage_warn_threshold ?? 80}
|
||
onChange={handleNumChange('storage_warn_threshold' as any)}
|
||
/>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Kritické při (%)</FormLabel>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
value={(settings as any).storage_critical_threshold ?? 95}
|
||
onChange={handleNumChange('storage_critical_threshold' as any)}
|
||
/>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Přednastavit</FormLabel>
|
||
<Button onClick={presetStorageThresholds} variant="outline">80 % / 95 %</Button>
|
||
</FormControl>
|
||
</SimpleGrid>
|
||
<FormControl>
|
||
<FormLabel>Logo klubu</FormLabel>
|
||
<HStack align="center" spacing={3}>
|
||
{settings.club_logo_url && (
|
||
<Image src={assetUrl(settings.club_logo_url) || settings.club_logo_url} alt="Logo" boxSize="56px" borderRadius="md" />
|
||
)}
|
||
<input ref={fileInputRef} type="file" accept="image/png,image/svg+xml,application/pdf" style={{ display: 'none' }} onChange={onLogoChosen} />
|
||
<Button onClick={onSelectLogo} variant="outline">Nahrát logo</Button>
|
||
</HStack>
|
||
</FormControl>
|
||
|
||
<Divider />
|
||
|
||
<Heading size="sm">Nastavení URL</Heading>
|
||
<FormControl>
|
||
<FormLabel>URL webu</FormLabel>
|
||
<Input value={(settings as any).frontend_base_url || ''} onChange={handleChange('frontend_base_url' as any)} placeholder="https://www.vasklub.cz" />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>API URL</FormLabel>
|
||
<Input value={(settings as any).api_base_url || ''} onChange={handleChange('api_base_url' as any)} placeholder="https://api.vasklub.cz/api/v1" />
|
||
<FormHelperText>Ujistěte se, že adresa končí na /api/v1</FormHelperText>
|
||
</FormControl>
|
||
|
||
<Heading size="sm">Zobrazení zápasů</Heading>
|
||
<FormControl>
|
||
<FormLabel>Počet dní zobrazení dokončených zápasů</FormLabel>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={30}
|
||
value={(settings as any).finished_match_display_days ?? 2}
|
||
onChange={(e) => {
|
||
const val = parseInt(e.target.value, 10);
|
||
setSettings((prev) => ({ ...prev, finished_match_display_days: isNaN(val) ? 2 : val } as any));
|
||
}}
|
||
/>
|
||
<FormHelperText>
|
||
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).
|
||
</FormHelperText>
|
||
</FormControl>
|
||
|
||
<HStack>
|
||
<Button onClick={handleSave} isLoading={saving} bg="brand.primary" color="text.onPrimary" _hover={{ filter: 'brightness(0.95)' }}>Uložit nastavení</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
<TabPanel>
|
||
<VStack align="stretch" spacing={4}>
|
||
<FormControl>
|
||
<FormLabel>Facebook URL</FormLabel>
|
||
<Input value={(settings as any).facebook_url || ''} onChange={handleChange('facebook_url')} />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Instagram URL</FormLabel>
|
||
<Input value={(settings as any).instagram_url || ''} onChange={handleChange('instagram_url')} />
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>YouTube URL</FormLabel>
|
||
<Input value={(settings as any).youtube_url || ''} onChange={handleChange('youtube_url')} />
|
||
<FormHelperText>Pro automatické načítání videí vyplňte YouTube kanál.</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Fotogalerie URL</FormLabel>
|
||
<Input value={(settings as any).gallery_url || ''} onChange={handleChange('gallery_url')} />
|
||
</FormControl>
|
||
|
||
<HStack>
|
||
<Button onClick={handleSave} isLoading={saving} bg="brand.primary" color="text.onPrimary" _hover={{ filter: 'brightness(0.95)' }}>Uložit nastavení</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
<TabPanel>
|
||
<VStack align="stretch" spacing={4}>
|
||
<Box p={4} bg="blue.50" borderRadius="md" borderLeft="4px solid" borderColor="blue.500">
|
||
<HStack spacing={2} mb={2}>
|
||
<Text fontWeight="bold" fontSize="sm">ℹ️ Sekce Videa</Text>
|
||
{(settings as any).videos_module_enabled && (
|
||
<Badge colorScheme="green">Aktivní</Badge>
|
||
)}
|
||
</HStack>
|
||
<Text fontSize="sm" color="gray.700">
|
||
Videa spravujte na stránce{' '}
|
||
<Button
|
||
as="a"
|
||
href="/admin/videa"
|
||
variant="link"
|
||
colorScheme="blue"
|
||
size="sm"
|
||
fontWeight="bold"
|
||
>
|
||
Videa
|
||
</Button>
|
||
. Zde nastavte zobrazení na titulní stránce.
|
||
{(settings as any).youtube_url && (
|
||
<Text as="span" color="green.700" fontWeight="semibold" ml={1}>
|
||
Sekce se automaticky aktivuje při vyplnění YouTube kanálu v tabě Sociální sítě.
|
||
</Text>
|
||
)}
|
||
</Text>
|
||
</Box>
|
||
|
||
<FormControl display="flex" alignItems="center">
|
||
<FormLabel mb={0}>Zobrazit sekci Videa na titulní stránce</FormLabel>
|
||
<Switch isChecked={!!(settings as any).videos_module_enabled} onChange={handleBoolChange('videos_module_enabled')} colorScheme="green" />
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>Zdroj videí</FormLabel>
|
||
<Select value={(settings as any).videos_source || 'auto'} onChange={handleSelectChange('videos_source')}>
|
||
<option value="auto">Automaticky (YouTube kanál)</option>
|
||
<option value="manual">Ručně (správa v sekci Videa)</option>
|
||
</Select>
|
||
<FormHelperText>Pro automatický zdroj vyplňte v tabě „Sociální sítě" pole YouTube URL.</FormHelperText>
|
||
</FormControl>
|
||
|
||
<HStack>
|
||
<Button onClick={handleSave} isLoading={saving} bg="brand.primary" color="text.onPrimary" _hover={{ filter: 'brightness(0.95)' }}>Uložit nastavení</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
<TabPanel>
|
||
<VStack align="stretch" spacing={4}>
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||
<FormControl>
|
||
<FormLabel>SMTP Host</FormLabel>
|
||
<Input
|
||
value={(settings as any).smtp_host || ''}
|
||
onChange={handleChange('smtp_host' as any)}
|
||
placeholder="smtp.seznam.cz nebo smtp.gmail.com"
|
||
/>
|
||
<FormHelperText>
|
||
Adresa SMTP serveru. Příklad: smtp.seznam.cz, smtp.gmail.com, smtp.office365.com
|
||
</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>SMTP Port</FormLabel>
|
||
<Input
|
||
type="number"
|
||
value={(settings as any).smtp_port ?? ''}
|
||
onChange={handleNumChange('smtp_port' as any)}
|
||
placeholder="587 pro TLS, 465 pro SSL, 25 bez šifrování"
|
||
/>
|
||
<FormHelperText>
|
||
Nejčastěji 587 (TLS/STARTTLS) nebo 465 (SSL). Port 25 je bez šifrování a často blokovaný.
|
||
</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>SMTP Uživatel</FormLabel>
|
||
<Input
|
||
value={(settings as any).smtp_user || ''}
|
||
onChange={handleChange('smtp_user' as any)}
|
||
placeholder="většinou celá e‑mailová adresa"
|
||
/>
|
||
<FormHelperText>
|
||
Přihlašovací jméno k SMTP (obvykle e‑mailová adresa). Nechte prázdné, pokud server nevyžaduje přihlášení.
|
||
</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>SMTP Heslo</FormLabel>
|
||
<Input
|
||
type="password"
|
||
value={(settings as any).smtp_password || ''}
|
||
onChange={handleChange('smtp_password' as any)}
|
||
placeholder="heslo nebo aplikační heslo"
|
||
/>
|
||
<FormHelperText>
|
||
Heslo k účtu nebo aplikační heslo (Gmail/Seznam/Office 365 často vyžadují). Vkládejte bez mezer.
|
||
</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>From e‑mail</FormLabel>
|
||
<Input
|
||
value={(settings as any).smtp_from || ''}
|
||
onChange={handleChange('smtp_from' as any)}
|
||
placeholder="noreply@vasklub.cz"
|
||
/>
|
||
<FormHelperText>
|
||
Adresa odesílatele uvedená v e‑mailech. Ideálně existující schránka na vašem SMTP serveru.
|
||
</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>From jméno</FormLabel>
|
||
<Input
|
||
value={(settings as any).smtp_from_name || ''}
|
||
onChange={handleChange('smtp_from_name' as any)}
|
||
placeholder="FK Váš Klub"
|
||
/>
|
||
<FormHelperText>
|
||
Zobrazované jméno odesílatele (např. FK Váš Klub).
|
||
</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Šifrování</FormLabel>
|
||
<Select value={(settings as any).smtp_encryption || 'none'} onChange={handleSelectChange('smtp_encryption' as any)}>
|
||
<option value="none">Žádné</option>
|
||
<option value="ssl">SSL</option>
|
||
<option value="tls">TLS</option>
|
||
</Select>
|
||
<FormHelperText>
|
||
SSL = implicitní šifrování (obvykle port 465). TLS/STARTTLS = šifrování po navázání spojení (obvykle port 587).
|
||
</FormHelperText>
|
||
</FormControl>
|
||
<FormControl>
|
||
<FormLabel>Autentizace</FormLabel>
|
||
<Select value={(settings as any).smtp_auth || 'plain'} onChange={handleSelectChange('smtp_auth' as any)}>
|
||
<option value="plain">PLAIN</option>
|
||
<option value="login">LOGIN</option>
|
||
<option value="cram-md5">CRAM‑MD5</option>
|
||
</Select>
|
||
<FormHelperText>
|
||
Mechanismus přihlášení k SMTP. Pokud si nejste jisti, zvolte PLAIN nebo LOGIN. Někteří poskytovatelé vyžadují konkrétní metodu.
|
||
</FormHelperText>
|
||
</FormControl>
|
||
</SimpleGrid>
|
||
<FormControl display="flex" alignItems="center">
|
||
<FormLabel mb={0}>Přeskočit ověření certifikátu</FormLabel>
|
||
<Switch isChecked={!!(settings as any).smtp_skip_verify} onChange={handleBoolChange('smtp_skip_verify' as any)} />
|
||
<FormHelperText ml={{ base: 0, md: 4 }}>
|
||
Pokročilé: povolte pouze při chybách certifikátu (self‑signed apod.). Nedoporučeno v produkci – snižuje bezpečnost.
|
||
</FormHelperText>
|
||
</FormControl>
|
||
|
||
<Divider />
|
||
<Heading size="sm">Test e‑mail</Heading>
|
||
<HStack>
|
||
<Input placeholder="test@example.com (volitelné)" value={testEmail} onChange={(e)=> setTestEmail(e.target.value)} maxW={{ base: '100%', md: '320px' }} />
|
||
<Button onClick={sendSmtpTest} isLoading={testing}>Odeslat test</Button>
|
||
</HStack>
|
||
|
||
<Divider />
|
||
<HStack>
|
||
<Button onClick={handleSave} isLoading={saving} bg="brand.primary" color="text.onPrimary" _hover={{ filter: 'brightness(0.95)' }}>Uložit nastavení</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
<TabPanel>
|
||
<VStack align="stretch" spacing={4}>
|
||
<Heading size="sm">Webová analytika</Heading>
|
||
<Text fontSize="sm" color="gray.600">
|
||
Nastavení sledování návštěvnosti webu pomocí open-source nástroje pro webovou analytiku.
|
||
</Text>
|
||
|
||
{umamiConfig?.enabled && umamiConfig?.website_id ? (
|
||
<Box p={4} borderWidth="1px" borderRadius="md" bg="green.50" borderColor="green.200">
|
||
<HStack spacing={2} mb={2}>
|
||
<Text fontWeight="bold" color="green.700">✓ Analytika je aktivní</Text>
|
||
</HStack>
|
||
<Text fontSize="sm" color="gray.700">Website ID: <strong>{umamiConfig.website_id}</strong></Text>
|
||
</Box>
|
||
) : (
|
||
<Box p={4} borderWidth="1px" borderRadius="md" bg="yellow.50" borderColor="yellow.200">
|
||
<Text fontWeight="bold" color="yellow.700">⚠ Analytika není nakonfigurována</Text>
|
||
<Text fontSize="sm" color="gray.700" mt={1}>Vytvořte webovou stránku pro aktivaci sledování.</Text>
|
||
</Box>
|
||
)}
|
||
|
||
<Divider />
|
||
|
||
<Heading size="sm">Vytvořit novou webovou stránku</Heading>
|
||
<FormControl>
|
||
<FormLabel>Název webové stránky</FormLabel>
|
||
<Input
|
||
value={umamiName}
|
||
onChange={(e) => setUmamiName(e.target.value)}
|
||
placeholder={settings.club_name || 'Fotbal Club'}
|
||
/>
|
||
<FormHelperText>Název, který se zobrazí v dashboardu analytiky</FormHelperText>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>Doména</FormLabel>
|
||
<Input
|
||
value={umamiDomain}
|
||
onChange={(e) => setUmamiDomain(e.target.value)}
|
||
placeholder={window.location.hostname}
|
||
/>
|
||
<FormHelperText>Doména vašeho webu (např. fotbal.example.com)</FormHelperText>
|
||
</FormControl>
|
||
|
||
<HStack>
|
||
<Button
|
||
onClick={async () => {
|
||
if (!umamiName || !umamiDomain) {
|
||
toast({
|
||
title: 'Chyba',
|
||
description: 'Vyplňte název i doménu',
|
||
status: 'error',
|
||
});
|
||
return;
|
||
}
|
||
setUmamiInitializing(true);
|
||
try {
|
||
const response = await api.post('/admin/insights/initialize', {
|
||
name: umamiName,
|
||
domain: umamiDomain,
|
||
});
|
||
setUmamiWebsiteId(response.data.website_id);
|
||
toast({
|
||
title: 'Úspěch',
|
||
description: `Webová stránka pro analytiku vytvořena! ID: ${response.data.website_id}`,
|
||
status: 'success',
|
||
duration: 5000,
|
||
});
|
||
// Refresh config
|
||
await fetchUmamiConfig();
|
||
} catch (error: any) {
|
||
const errorMsg = error?.response?.data?.error || error.message || 'Vytvoření selhalo';
|
||
toast({
|
||
title: 'Chyba',
|
||
description: errorMsg,
|
||
status: 'error',
|
||
duration: 7000,
|
||
});
|
||
} finally {
|
||
setUmamiInitializing(false);
|
||
}
|
||
}}
|
||
isLoading={umamiInitializing}
|
||
bg="brand.primary"
|
||
color="text.onPrimary"
|
||
_hover={{ filter: 'brightness(0.95)' }}
|
||
>
|
||
Vytvořit webovou stránku
|
||
</Button>
|
||
<Button
|
||
onClick={fetchUmamiConfig}
|
||
variant="outline"
|
||
>
|
||
Aktualizovat stav
|
||
</Button>
|
||
</HStack>
|
||
|
||
<Text fontSize="xs" color="gray.500" mt={2}>
|
||
Poznámka: Pro aktivaci analytiky je nutné mít nastavené přihlašovací údaje v .env souboru.
|
||
</Text>
|
||
</VStack>
|
||
</TabPanel>
|
||
|
||
<TabPanel>
|
||
<VStack align="stretch" spacing={4}>
|
||
<Heading size="sm">SEO nastavení</Heading>
|
||
<Text fontSize="sm" color="gray.600" mb={4}>
|
||
Základní SEO nastavení pro lepší viditelnost ve vyhledávačích.
|
||
</Text>
|
||
|
||
<FormControl>
|
||
<FormLabel>Titulek webu</FormLabel>
|
||
<Input value={seo.site_title || ''} onChange={handleSeoChange('site_title')} />
|
||
<FormHelperText>Hlavní název webu, zobrazí se v záložce prohlížeče a ve výsledcích vyhledávání</FormHelperText>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>Popis webu</FormLabel>
|
||
<Textarea
|
||
value={(seo as any).site_description || ''}
|
||
onChange={(e) => setSeo((prev) => ({ ...prev, site_description: e.target.value } as any))}
|
||
rows={3}
|
||
/>
|
||
<FormHelperText>Krátký popis webu pro vyhledávače (doporučeno 150-160 znaků)</FormHelperText>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>Klíčová slova</FormLabel>
|
||
<Input
|
||
value={(seo as any).keywords || ''}
|
||
onChange={(e) => setSeo((prev) => ({ ...prev, keywords: e.target.value } as any))}
|
||
/>
|
||
<FormHelperText>Klíčová slova oddělená čárkou (např: fotbal, klub, sport)</FormHelperText>
|
||
</FormControl>
|
||
|
||
<Divider />
|
||
|
||
<Heading size="sm">Open Graph (sociální sítě)</Heading>
|
||
|
||
<FormControl>
|
||
<FormLabel>Výchozí OG obrázek (URL)</FormLabel>
|
||
<Input value={seo.default_og_image_url || ''} onChange={handleSeoChange('default_og_image_url')} />
|
||
<FormHelperText>Výchozí obrázek pro sdílení na sociálních sítích (doporučeno 1200x630px)</FormHelperText>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>OG Type</FormLabel>
|
||
<Select
|
||
value={(seo as any).og_type || 'website'}
|
||
onChange={(e) => setSeo((prev) => ({ ...prev, og_type: e.target.value } as any))}
|
||
>
|
||
<option value="website">Website</option>
|
||
<option value="article">Article</option>
|
||
<option value="profile">Profile</option>
|
||
</Select>
|
||
<FormHelperText>Typ obsahu pro Open Graph</FormHelperText>
|
||
</FormControl>
|
||
|
||
<Divider />
|
||
|
||
<Heading size="sm">Twitter Card</Heading>
|
||
|
||
<FormControl>
|
||
<FormLabel>Twitter Card Type</FormLabel>
|
||
<Select
|
||
value={(seo as any).twitter_card || 'summary_large_image'}
|
||
onChange={(e) => setSeo((prev) => ({ ...prev, twitter_card: e.target.value } as any))}
|
||
>
|
||
<option value="summary">Summary</option>
|
||
<option value="summary_large_image">Summary Large Image</option>
|
||
</Select>
|
||
<FormHelperText>Typ karty pro Twitter</FormHelperText>
|
||
</FormControl>
|
||
|
||
<FormControl>
|
||
<FormLabel>Twitter Site</FormLabel>
|
||
<Input
|
||
value={(seo as any).twitter_site || ''}
|
||
onChange={(e) => setSeo((prev) => ({ ...prev, twitter_site: e.target.value } as any))}
|
||
placeholder="@nazev_uctu"
|
||
/>
|
||
<FormHelperText>Twitter účet webu (např. @fotbalklub)</FormHelperText>
|
||
</FormControl>
|
||
|
||
<HStack>
|
||
<Button onClick={saveSEO} bg="brand.primary" color="text.onPrimary" _hover={{ filter: 'brightness(0.95)' }}>Uložit SEO</Button>
|
||
</HStack>
|
||
</VStack>
|
||
</TabPanel>
|
||
</TabPanels>
|
||
</Tabs>
|
||
|
||
<Divider my={4} />
|
||
<HStack>
|
||
<Button onClick={handleSave} isLoading={saving} bg="brand.primary" color="text.onPrimary" _hover={{ filter: 'brightness(0.95)' }}>Uložit nastavení</Button>
|
||
</HStack>
|
||
</Box>
|
||
</AdminLayout>
|
||
);
|
||
};
|
||
|
||
export default SettingsAdminPage;
|