Files
MyClub/frontend/src/pages/admin/SettingsAdminPage.tsx
T
Tomas Dvorak e9a63073e5 dev day #63
2025-10-17 17:39:11 +02:00

642 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('/umami/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ý email.', 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 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,
gallery_label: (settings as any).gallery_label,
// 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,
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,
// Auto-enable map display if coordinates are set
show_map_on_homepage: ((settings as any).location_latitude && (settings as any).location_longitude) as any,
map_style: (settings as any).map_style,
// homepage matches display
finished_match_display_days: (settings as any).finished_match_display_days as any,
};
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 {}
} 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>
<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">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>
<FormControl>
<FormLabel>Popisek fotogalerie</FormLabel>
<Input value={(settings as any).gallery_label || ''} onChange={handleChange('gallery_label')} />
</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)} />
</FormControl>
<FormControl>
<FormLabel>SMTP Port</FormLabel>
<Input type="number" value={(settings as any).smtp_port ?? ''} onChange={handleNumChange('smtp_port' as any)} />
</FormControl>
<FormControl>
<FormLabel>SMTP Uživatel</FormLabel>
<Input value={(settings as any).smtp_user || ''} onChange={handleChange('smtp_user' as any)} />
</FormControl>
<FormControl>
<FormLabel>SMTP Heslo</FormLabel>
<Input type="password" value={(settings as any).smtp_password || ''} onChange={handleChange('smtp_password' as any)} />
</FormControl>
<FormControl>
<FormLabel>From email</FormLabel>
<Input value={(settings as any).smtp_from || ''} onChange={handleChange('smtp_from' as any)} />
</FormControl>
<FormControl>
<FormLabel>From jméno</FormLabel>
<Input value={(settings as any).smtp_from_name || ''} onChange={handleChange('smtp_from_name' as any)} />
</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>
</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">CRAMMD5</option>
</Select>
</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)} />
</FormControl>
<Divider />
<Heading size="sm">Test email</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/umami/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;