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,912 @@
|
||||
import { useEffect, useState, useMemo, startTransition } from 'react';
|
||||
import { Box, Button, FormControl, FormLabel, Input, VStack, Heading, useToast, SimpleGrid, Divider, Text, useColorModeValue, InputGroup, InputRightElement, List, ListItem, Spinner, HStack, Image, Checkbox, Tooltip, Alert, AlertIcon, Select, FormHelperText, Badge, Link } from '@chakra-ui/react';
|
||||
import { InfoOutlineIcon } from '@chakra-ui/icons';
|
||||
import './styles/MagazineHome.css';
|
||||
import './styles/ProHome.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getSetupStatus, initializeSetup, SetupInitializePayload, validateSMTP } from '../services/setup';
|
||||
import { updateSeoSettings } from '../services/seo';
|
||||
import { API_URL } from '../services/api';
|
||||
import { assetUrl } from '../utils/url';
|
||||
import { useFacrApi } from '../hooks/useFacrApi';
|
||||
import { SearchResult } from '../services/facr/types';
|
||||
import { extractPalette, pickTextColor, generateJwtSecret, contrastRatio, isContrastAccessible, generateThemeCandidates, ThemeCandidate, adjustForContrast } from '../utils/colors';
|
||||
import { clearToken, setHasAdmin } from '../utils/auth';
|
||||
import ContactMap from '../components/home/ContactMap';
|
||||
import { FONT_PAIRINGS, loadGoogleFont, getFontStyleColor } from '../config/fonts';
|
||||
import MapLinkImporter from '../components/admin/MapLinkImporter';
|
||||
import MapStyleSelector from '../components/admin/MapStyleSelector';
|
||||
import { MapCoordinates } from '../utils/mapUrlParser';
|
||||
|
||||
const SetupPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [requiresSetup, setRequiresSetup] = useState(false);
|
||||
|
||||
// form state
|
||||
const [adminEmail, setAdminEmail] = useState('');
|
||||
const [adminPassword, setAdminPassword] = useState('');
|
||||
const [showAdminPassword, setShowAdminPassword] = useState(false);
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [jwtSecret, setJwtSecret] = useState('');
|
||||
const [clubId, setClubId] = useState('');
|
||||
const [clubType, setClubType] = useState('football');
|
||||
const [clubName, setClubName] = useState('');
|
||||
const [clubLogoUrl, setClubLogoUrl] = useState('');
|
||||
const [uploadingLogo, setUploadingLogo] = useState(false);
|
||||
const [clubUrl, setClubUrl] = useState('');
|
||||
const [clubQuery, setClubQuery] = useState('');
|
||||
const { searchClubs, searchResults, searchLoading } = useFacrApi();
|
||||
|
||||
const resolveLogoUrl = (u?: string | null) => {
|
||||
if (!u) return undefined;
|
||||
// If it's a backend-relative path or dist asset, use assetUrl helper
|
||||
if (u.startsWith('/uploads') || u.startsWith('/dist') || u.startsWith('/api/')) return assetUrl(u);
|
||||
// If it's an absolute remote URL, route through backend proxy to avoid CORS/hotlinking issues
|
||||
if (/^https?:\/\//i.test(u)) {
|
||||
const base = (API_URL || '').replace(/\/$/, '');
|
||||
return `${base}/proxy/image?url=${encodeURIComponent(u)}`;
|
||||
}
|
||||
return u;
|
||||
};
|
||||
|
||||
// Theme colors
|
||||
// Default theme colors before any logo/preset is selected
|
||||
const [primaryColor, setPrimaryColor] = useState('#2d74da');
|
||||
const [secondaryColor, setSecondaryColor] = useState('#f6f8fb');
|
||||
const [accentColor, setAccentColor] = useState('#ffb703');
|
||||
const [backgroundColor, setBackgroundColor] = useState('#ffffff');
|
||||
const [textColor, setTextColor] = useState('#111827');
|
||||
// Site style
|
||||
const [frontpageStyle, setFrontpageStyle] = useState<'unified' | 'magazine'>('unified');
|
||||
// Theme presets generated from logo
|
||||
const [themePresets, setThemePresets] = useState<ThemeCandidate[]>([]);
|
||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
|
||||
|
||||
// Typography/Font
|
||||
const [selectedFont, setSelectedFont] = useState<string>('inter-inter');
|
||||
|
||||
// GPS Location
|
||||
const [mapUrl, setMapUrl] = useState('');
|
||||
const [gpsLat, setGpsLat] = useState<number | ''>('');
|
||||
const [gpsLng, setGpsLng] = useState<number | ''>('');
|
||||
const [mapStyle, setMapStyle] = useState<string>('positron');
|
||||
|
||||
// Contact Details
|
||||
const [contactStreet, setContactStreet] = useState('');
|
||||
const [contactCity, setContactCity] = useState('');
|
||||
const [contactPostalCode, setContactPostalCode] = useState('');
|
||||
const [contactCountry, setContactCountry] = useState('Česká republika');
|
||||
const [contactPhone, setContactPhone] = useState('');
|
||||
const [contactEmail, setContactEmail] = useState('');
|
||||
|
||||
// SMTP
|
||||
const [smtpHost, setSmtpHost] = useState('');
|
||||
const [smtpPort, setSmtpPort] = useState<number | ''>('');
|
||||
const [smtpUser, setSmtpUser] = useState('');
|
||||
const [smtpPass, setSmtpPass] = useState('');
|
||||
const [showSmtpPass, setShowSmtpPass] = useState(false);
|
||||
// Sender display name only; actual email is derived from smtpUser
|
||||
const [smtpFromName, setSmtpFromName] = useState('');
|
||||
const [smtpTLS, setSmtpTLS] = useState(true);
|
||||
const [testingSMTP, setTestingSMTP] = useState(false);
|
||||
|
||||
// Social & gallery
|
||||
const [facebookUrl, setFacebookUrl] = useState('');
|
||||
const [instagramUrl, setInstagramUrl] = useState('');
|
||||
const [youtubeUrl, setYoutubeUrl] = useState('');
|
||||
const [galleryUrl, setGalleryUrl] = useState('');
|
||||
const [galleryLabel, setGalleryLabel] = useState('Fotogalerie');
|
||||
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const bg = useColorModeValue('white', 'gray.800');
|
||||
const borderCol = useColorModeValue('gray.200', 'gray.600');
|
||||
|
||||
// Password helpers
|
||||
const sanitizePassword = (val: string) => {
|
||||
// remove all whitespace characters (spaces, tabs, newlines, non-breaking spaces)
|
||||
return (val || '').replace(/\s+/g, '');
|
||||
};
|
||||
const isPasswordValid = (val: string) => {
|
||||
// 8-128 chars, no whitespace
|
||||
return /^\S{8,128}$/.test(val);
|
||||
};
|
||||
const generateStrongPassword = () => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*()-_=+[]{};:,.?';
|
||||
let out = '';
|
||||
const len = 16;
|
||||
for (let i = 0; i < len; i++) {
|
||||
out += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
// Ensure no stale auth/admin flags can interfere with setup
|
||||
try {
|
||||
clearToken();
|
||||
setHasAdmin(false);
|
||||
} catch {}
|
||||
(async () => {
|
||||
try {
|
||||
const s = await getSetupStatus();
|
||||
if (!mounted) return;
|
||||
setRequiresSetup(!!s.requires_setup);
|
||||
} catch (e) {
|
||||
setRequiresSetup(false);
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
// Auto-generate JWT secret when setup is required
|
||||
useEffect(() => {
|
||||
if (requiresSetup && !jwtSecret) {
|
||||
setJwtSecret(generateJwtSecret());
|
||||
}
|
||||
}, [requiresSetup, jwtSecret]);
|
||||
|
||||
// Debounced search for clubs
|
||||
useEffect(() => {
|
||||
const q = clubQuery.trim();
|
||||
if (!q) return;
|
||||
const t = setTimeout(() => {
|
||||
searchClubs(q).catch(() => {});
|
||||
}, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [clubQuery, searchClubs]);
|
||||
|
||||
// Load selected font for preview
|
||||
useEffect(() => {
|
||||
const pairing = FONT_PAIRINGS.find((f) => f.id === selectedFont);
|
||||
if (pairing) {
|
||||
loadGoogleFont(pairing.googleFontsUrl);
|
||||
}
|
||||
}, [selectedFont]);
|
||||
|
||||
const handleSelectClub = (item: SearchResult) => {
|
||||
setClubId(item.club_id || '');
|
||||
setClubType(item.club_type || 'football');
|
||||
setClubName(item.name || '');
|
||||
setClubLogoUrl(item.logo_url || '');
|
||||
setClubUrl(item.url || '');
|
||||
setClubQuery(item.name || '');
|
||||
// Auto-fill sender display name from club name if empty
|
||||
if (!smtpFromName && item.name) {
|
||||
setSmtpFromName(item.name);
|
||||
}
|
||||
// Update the document title immediately for instant feedback
|
||||
try {
|
||||
if (typeof document !== 'undefined' && item.name) {
|
||||
document.title = item.name;
|
||||
}
|
||||
} catch {}
|
||||
// Try to extract colors
|
||||
if (item.logo_url) {
|
||||
extractPalette(item.logo_url, 5)
|
||||
.then((colors) => {
|
||||
if (!colors || colors.length === 0) return;
|
||||
const presets = generateThemeCandidates(colors);
|
||||
setThemePresets(presets);
|
||||
// Apply first preset by default
|
||||
const p0 = presets[0];
|
||||
if (p0) {
|
||||
setPrimaryColor(p0.primary);
|
||||
setSecondaryColor(p0.secondary);
|
||||
setAccentColor(p0.accent);
|
||||
setBackgroundColor(p0.background);
|
||||
setTextColor(p0.text);
|
||||
setSelectedPreset(0);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
// Validate and sanitize password
|
||||
const cleanPassword = sanitizePassword(adminPassword);
|
||||
if (!isPasswordValid(cleanPassword)) {
|
||||
toast({ title: 'Neplatné heslo', description: 'Heslo musí mít 8–128 znaků a nesmí obsahovat mezery.', status: 'error' });
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
// Compose SMTP From as "Name <username>" if both provided
|
||||
const composedFrom = smtpFromName && smtpUser
|
||||
? `${smtpFromName} <${smtpUser}>`
|
||||
: (smtpUser || undefined);
|
||||
|
||||
const payload: SetupInitializePayload = {
|
||||
admin_email: adminEmail,
|
||||
admin_password: cleanPassword,
|
||||
first_name: firstName || undefined,
|
||||
last_name: lastName || undefined,
|
||||
jwt_secret: jwtSecret || undefined,
|
||||
club_id: clubId || undefined,
|
||||
club_type: clubType || undefined,
|
||||
club_name: clubName || undefined,
|
||||
club_logo_url: clubLogoUrl || undefined,
|
||||
club_url: clubUrl || undefined,
|
||||
frontpage_style: frontpageStyle || undefined,
|
||||
primary_color: primaryColor || undefined,
|
||||
secondary_color: secondaryColor || undefined,
|
||||
accent_color: accentColor || undefined,
|
||||
background_color: backgroundColor || undefined,
|
||||
text_color: textColor || undefined,
|
||||
// typography (optional)
|
||||
font_heading: FONT_PAIRINGS.find(f => f.id === selectedFont)?.heading || undefined,
|
||||
font_body: FONT_PAIRINGS.find(f => f.id === selectedFont)?.body || undefined,
|
||||
// social & gallery (optional)
|
||||
facebook_url: facebookUrl || undefined,
|
||||
instagram_url: instagramUrl || undefined,
|
||||
youtube_url: youtubeUrl || undefined,
|
||||
gallery_url: galleryUrl || undefined,
|
||||
gallery_label: galleryLabel || undefined,
|
||||
// GPS location (optional)
|
||||
location_latitude: typeof gpsLat === 'number' ? gpsLat : undefined,
|
||||
location_longitude: typeof gpsLng === 'number' ? gpsLng : undefined,
|
||||
map_style: mapStyle || undefined,
|
||||
// Contact details (optional)
|
||||
contact_address: contactStreet || undefined,
|
||||
contact_city: contactCity || undefined,
|
||||
contact_zip: contactPostalCode || undefined,
|
||||
contact_country: contactCountry || undefined,
|
||||
contact_phone: contactPhone || undefined,
|
||||
contact_email: contactEmail || undefined,
|
||||
smtp: (smtpHost || smtpPort || smtpUser || smtpPass || smtpFromName) ? {
|
||||
host: smtpHost || undefined,
|
||||
port: typeof smtpPort === 'number' ? smtpPort : undefined,
|
||||
username: smtpUser || undefined,
|
||||
password: smtpPass || undefined,
|
||||
from: composedFrom,
|
||||
use_tls: smtpTLS,
|
||||
} : null,
|
||||
};
|
||||
await initializeSetup(payload);
|
||||
// Set sensible default SEO based on setup data
|
||||
try {
|
||||
const origin = (typeof window !== 'undefined' ? window.location.origin : '').replace(/\/$/, '');
|
||||
await updateSeoSettings({
|
||||
site_title: clubName || 'Fotbal Club',
|
||||
site_description: clubName ? `${clubName} – oficiální klubový web: aktuality, zápasy, tabulky, hráči.` : 'Oficiální klubový web: aktuality, zápasy, tabulky, hráči.',
|
||||
default_og_image_url: clubLogoUrl || undefined,
|
||||
canonical_base_url: origin || undefined,
|
||||
enable_indexing: true,
|
||||
});
|
||||
} catch {}
|
||||
toast({ title: 'Nastavení dokončeno', status: 'success', duration: 3000, isClosable: true });
|
||||
navigate('/login', { replace: true });
|
||||
// Force full reload to ensure app picks up fresh server state and env
|
||||
setTimeout(() => {
|
||||
try { window.location.reload(); } catch {}
|
||||
}, 800);
|
||||
} catch (error: any) {
|
||||
toast({ title: 'Nastavení selhalo', description: error?.response?.data?.error || 'Zkontrolujte prosím své údaje', status: 'error' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onLogoFile = async (f?: File | null) => {
|
||||
if (!f) return;
|
||||
setUploadingLogo(true);
|
||||
try {
|
||||
// Use a dedicated form so we can pass preserve_quality to backend
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
fd.append('preserve_quality', 'true');
|
||||
// Upload should go to the API root (usually /api/v1/upload). Use configured API_URL
|
||||
const uploadUrl = `${(API_URL || '').replace(/\/$/, '')}/upload`;
|
||||
const res = await fetch(uploadUrl, { method: 'POST', body: fd });
|
||||
if (!res.ok) throw new Error('Upload failed');
|
||||
const data = await res.json();
|
||||
let url = data?.url || '';
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
url = parsed.pathname + parsed.search + parsed.hash;
|
||||
} catch {}
|
||||
setClubLogoUrl(url);
|
||||
// Try to extract colors from uploaded logo
|
||||
try { const colors = await extractPalette(url, 5); const presets = generateThemeCandidates(colors); setThemePresets(presets); if (presets[0]) { setPrimaryColor(presets[0].primary); setSecondaryColor(presets[0].secondary); setAccentColor(presets[0].accent); setBackgroundColor(presets[0].background); setTextColor(presets[0].text); setSelectedPreset(0); } } catch {}
|
||||
} catch (e) {
|
||||
toast({ title: 'Nahrání loga selhalo', status: 'error' });
|
||||
} finally {
|
||||
setUploadingLogo(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Derived: contrast checks (memoized for perf)
|
||||
const bgTextContrast = useMemo(() => {
|
||||
if (!backgroundColor || !textColor) return null;
|
||||
return contrastRatio(backgroundColor, textColor);
|
||||
}, [backgroundColor, textColor]);
|
||||
|
||||
const primaryTextContrast = useMemo(() => {
|
||||
if (!primaryColor || !textColor) return null;
|
||||
return contrastRatio(primaryColor, textColor);
|
||||
}, [primaryColor, textColor]);
|
||||
|
||||
const isBgTextAccessible = useMemo(() => {
|
||||
if (bgTextContrast == null) return true;
|
||||
return isContrastAccessible(backgroundColor, textColor, 'AA');
|
||||
}, [bgTextContrast, backgroundColor, textColor]);
|
||||
|
||||
const autofixTextForBg = () => {
|
||||
if (!backgroundColor) return;
|
||||
const fixed = pickTextColor(backgroundColor);
|
||||
setTextColor(fixed);
|
||||
};
|
||||
|
||||
// Re-generate presets manually when user clicks button (from current logo URL)
|
||||
const regenerateFromLogo = async () => {
|
||||
const url = clubLogoUrl?.trim();
|
||||
if (!url) return;
|
||||
try {
|
||||
const colors = await extractPalette(url, 5);
|
||||
const presets = generateThemeCandidates(colors);
|
||||
setThemePresets(presets);
|
||||
if (presets[0]) {
|
||||
setPrimaryColor(presets[0].primary);
|
||||
setSecondaryColor(presets[0].secondary);
|
||||
setAccentColor(presets[0].accent);
|
||||
setBackgroundColor(presets[0].background);
|
||||
setTextColor(presets[0].text);
|
||||
setSelectedPreset(0);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const applyPreset = (idx: number) => {
|
||||
const p = themePresets[idx];
|
||||
if (!p) return;
|
||||
setPrimaryColor(p.primary);
|
||||
setSecondaryColor(p.secondary);
|
||||
setAccentColor(p.accent);
|
||||
setBackgroundColor(p.background);
|
||||
setTextColor(p.text);
|
||||
setSelectedPreset(idx);
|
||||
};
|
||||
|
||||
if (loading) return <Box p={8}>Načítání…</Box>;
|
||||
if (!requiresSetup) {
|
||||
navigate('/login', { replace: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box minH="100vh" bg="gray.50" display="flex" alignItems="center" justifyContent="center" px={8} py={8}>
|
||||
<Box as="form" onSubmit={handleSubmit} w="100%" maxW="3xl" p={8} bg={bg} borderRadius="xl" boxShadow="lg" borderWidth="1px" borderColor={borderCol}>
|
||||
<VStack spacing={3} mb={6} align="stretch">
|
||||
<Heading size="xl">🚀 Vítejte v nastavení vašeho webu!</Heading>
|
||||
<Text fontSize="md" color="gray.600">
|
||||
Nastavte základní informace o vašem klubu. Můžete vše vyplnit nyní, nebo některé údaje doplnit později v administraci.
|
||||
</Text>
|
||||
<Alert status="info" borderRadius="md">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="medium">💡 Tip: Vyhledejte váš klub v databázi FAČR</Text>
|
||||
<Text fontSize="sm">Logo, barvy a základní údaje se doplní automaticky.</Text>
|
||||
</Box>
|
||||
</Alert>
|
||||
</VStack>
|
||||
|
||||
<SimpleGrid columns={[1, 1, 2]} spacing={6}>
|
||||
<Box>
|
||||
<Heading as="h3" size="md" mb={4}>🔐 Administrátorský účet</Heading>
|
||||
<VStack align="stretch" spacing={4}>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>E‑mail administrátora</FormLabel>
|
||||
<Input type="email" value={adminEmail} onChange={(e) => setAdminEmail(e.target.value)} placeholder="admin@example.com" />
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Heslo administrátora</FormLabel>
|
||||
<InputGroup>
|
||||
<Input
|
||||
type={showAdminPassword ? 'text' : 'password'}
|
||||
value={adminPassword}
|
||||
onChange={(e) => setAdminPassword(sanitizePassword(e.target.value))}
|
||||
placeholder="Minimálně 8 znaků, bez mezer"
|
||||
minLength={8}
|
||||
/>
|
||||
<InputRightElement width="4.5rem">
|
||||
<Button h="1.75rem" size="sm" onClick={() => setShowAdminPassword((s) => !s)}>{showAdminPassword ? 'Skrýt' : 'Zobrazit'}</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
<FormHelperText>Bez mezer. 8–128 znaků. Použijte písmena, číslice a speciální znaky.</FormHelperText>
|
||||
<HStack mt={2} spacing={2}>
|
||||
<Button size="sm" variant="outline" onClick={() => setAdminPassword(generateStrongPassword())}>Vygenerovat silné heslo</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => setAdminPassword('')}>Vymazat</Button>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Jméno</FormLabel>
|
||||
<Input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Příjmení</FormLabel>
|
||||
<Input value={lastName} onChange={(e) => setLastName(e.target.value)} />
|
||||
</FormControl>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Heading as="h3" size="md" mb={4}>⚽ Informace o klubu</Heading>
|
||||
<VStack align="stretch" spacing={4}>
|
||||
<FormControl>
|
||||
<FormLabel>Hledat klub (FAČR)</FormLabel>
|
||||
<InputGroup>
|
||||
<Input value={clubQuery} onChange={(e) => setClubQuery(e.target.value)} placeholder="Hledejte podle názvu klubu" />
|
||||
<InputRightElement>
|
||||
{searchLoading ? <Spinner size="sm" /> : null}
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
{clubQuery && searchResults?.length > 0 && (
|
||||
<Box mt={2} borderWidth="1px" borderRadius="md" maxH="240px" overflowY="auto">
|
||||
<List spacing={0}>
|
||||
{searchResults.slice(0, 8).map((r) => (
|
||||
<ListItem
|
||||
key={`${r.club_type}-${r.club_id}`}
|
||||
px={3} py={2} _hover={{ bg: 'gray.50', cursor: 'pointer' }}
|
||||
onClick={() => handleSelectClub(r)}
|
||||
>
|
||||
<HStack spacing={3}>
|
||||
{r.logo_url ? <Image src={resolveLogoUrl(r.logo_url)} alt={r.name} boxSize="24px" objectFit="contain" /> : null}
|
||||
<Box>
|
||||
<Text fontWeight="medium">{r.name}</Text>
|
||||
<Text fontSize="sm" color="gray.500">{r.club_type}</Text>
|
||||
</Box>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Typ klubu</FormLabel>
|
||||
<Input value={clubType} onChange={(e) => setClubType(e.target.value)} placeholder="football nebo futsal" />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>ID klubu</FormLabel>
|
||||
<Input value={clubId} onChange={(e) => setClubId(e.target.value)} placeholder="FAČR ID klubu" />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Název klubu</FormLabel>
|
||||
<Input value={clubName} onChange={(e) => setClubName(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>URL loga klubu</FormLabel>
|
||||
<Input value={clubLogoUrl} onChange={(e) => setClubLogoUrl(e.target.value)} />
|
||||
<HStack mt={2} spacing={3}>
|
||||
<Button as="label" size="sm" leftIcon={<InfoOutlineIcon />}>
|
||||
Nahrát logo
|
||||
<Input
|
||||
type="file"
|
||||
display="none"
|
||||
accept="image/*,image/svg+xml,application/pdf"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0] || null;
|
||||
// Process selected file
|
||||
onLogoFile(f);
|
||||
// Important: reset input value so selecting the same file again fires onChange
|
||||
// This fixes the bug where uploading the same logo twice did nothing
|
||||
try { (e.target as HTMLInputElement).value = ''; } catch {}
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
{uploadingLogo ? <Text fontSize="sm">Nahrávám…</Text> : null}
|
||||
</HStack>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>URL klubu</FormLabel>
|
||||
<Input value={clubUrl} onChange={(e) => setClubUrl(e.target.value)} />
|
||||
</FormControl>
|
||||
<Box>
|
||||
<Text mb={2}>Náhled loga</Text>
|
||||
<Image src={resolveLogoUrl(clubLogoUrl) || (assetUrl('/dist/img/logo-club-empty.svg') as string)} alt="Logo preview" maxH="80px" objectFit="contain" />
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* removed overall style preview per request */}
|
||||
|
||||
<Divider my={6} />
|
||||
|
||||
<Heading as="h3" size="md" mb={2}>📱 Sociální sítě a fotogalerie</Heading>
|
||||
<Text fontSize="sm" mb={3} color="gray.600">Zadejte odkazy na profily klubu a volitelně na fotogalerii. Lze později upravit v administraci.</Text>
|
||||
<SimpleGrid columns={[1, 1, 2]} spacing={6} mb={2}>
|
||||
<FormControl>
|
||||
<FormLabel>Facebook URL</FormLabel>
|
||||
<Input placeholder="https://www.facebook.com/vas.klub" value={facebookUrl} onChange={(e) => setFacebookUrl(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Instagram URL</FormLabel>
|
||||
<Input placeholder="https://www.instagram.com/vas.klub" value={instagramUrl} onChange={(e) => setInstagramUrl(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>YouTube URL</FormLabel>
|
||||
<Input placeholder="https://www.youtube.com/@vas_klub" value={youtubeUrl} onChange={(e) => setYoutubeUrl(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>URL fotogalerie</FormLabel>
|
||||
<Input placeholder="https://photos.example.com/club" value={galleryUrl} onChange={(e) => setGalleryUrl(e.target.value)} />
|
||||
<FormHelperText>Můžete použít libovolný web (SmugMug, Flickr, Google Photos, Zonerama...).</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Popisek odkazu fotogalerie</FormLabel>
|
||||
<Input placeholder="Fotogalerie" value={galleryLabel} onChange={(e) => setGalleryLabel(e.target.value)} />
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
|
||||
<Heading as="h3" size="md" mb={2}>🎨 Barvy a vzhled webu</Heading>
|
||||
<Text fontSize="sm" mb={3} color="gray.600">Automaticky z loga (lze upravit). Vyberte jednu z předloh nebo barvy ručně dolaďte.</Text>
|
||||
|
||||
{/* Preset selector */}
|
||||
{themePresets.length > 0 && (
|
||||
<Box mb={4}>
|
||||
<Text fontWeight="semibold" mb={2}>Předlohy z loga</Text>
|
||||
<SimpleGrid columns={{ base: 1, md: 5 }} spacing={3}>
|
||||
{themePresets.map((p, i) => (
|
||||
<Box key={p.name + i} borderWidth={selectedPreset === i ? '2px' : '1px'} borderColor={selectedPreset === i ? 'blue.400' : 'gray.200'} borderRadius="md" p={3} cursor="pointer" onClick={() => applyPreset(i)}>
|
||||
<Box mb={2} display="flex" gap="6px" alignItems="center">
|
||||
<Box flex="1" h="48px" borderRadius="6px" bg={p.primary} display="flex" alignItems="center" justifyContent="center" color={p.text}>{/* primary */}</Box>
|
||||
<Box w="48px" h="48px" borderRadius="6px" bg={p.secondary} />
|
||||
<Box w="48px" h="48px" borderRadius="6px" bg={p.accent} />
|
||||
</Box>
|
||||
<Text fontSize="sm" fontWeight="semibold">{p.name}</Text>
|
||||
<Text fontSize="xs" color="gray.500">Primární / Sekundární / Akcent</Text>
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Button mt={3} variant="ghost" onClick={regenerateFromLogo}>Znovu z loga</Button>
|
||||
</Box>
|
||||
)}
|
||||
<SimpleGrid columns={[1, 1, 3]} spacing={6}>
|
||||
<FormControl>
|
||||
<FormLabel>Styl webu</FormLabel>
|
||||
<Select value={frontpageStyle} onChange={(e) => setFrontpageStyle((e.target.value as any) || 'unified')}>
|
||||
<option value="unified">Aktuální (Unified)</option>
|
||||
<option value="magazine">Nový (Magazine)</option>
|
||||
<option value="pro">Pro (Hero fullscreen)</option>
|
||||
<option value="edge">Edge (Full‑width minimal)</option>
|
||||
</Select>
|
||||
<FormHelperText>Zvolte výchozí vzhled. Lze později změnit v administraci.</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Primární
|
||||
<Tooltip label="Hlavní barva značky (tlačítka, odkazy, zvýraznění)." hasArrow><InfoOutlineIcon ml={2} /></Tooltip>
|
||||
</FormLabel>
|
||||
<Input type="color" value={primaryColor} onChange={(e) => startTransition(() => setPrimaryColor(e.target.value))} />
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>Používá se na hlavních prvcích.</Text>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Sekundární
|
||||
<Tooltip label="Doplňková barva pro méně důležité prvky a plochy." hasArrow><InfoOutlineIcon ml={2} /></Tooltip>
|
||||
</FormLabel>
|
||||
<Input type="color" value={secondaryColor} onChange={(e) => startTransition(() => setSecondaryColor(e.target.value))} />
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>Podpůrné zvýraznění.</Text>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Akcent
|
||||
<Tooltip label="Kontrastní barva pro odznaky, štítky a malé akce." hasArrow><InfoOutlineIcon ml={2} /></Tooltip>
|
||||
</FormLabel>
|
||||
<Input type="color" value={accentColor} onChange={(e) => startTransition(() => setAccentColor(e.target.value))} />
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>Menší prvky a upozornění.</Text>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Pozadí</FormLabel>
|
||||
<Input type="color" value={backgroundColor} onChange={(e) => startTransition(() => setBackgroundColor(e.target.value))} />
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>Barva ploch stránky.</Text>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Text</FormLabel>
|
||||
<Input type="color" value={textColor} onChange={(e) => startTransition(() => setTextColor(e.target.value))} />
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>Základní barva textu.</Text>
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* Contrast warnings */}
|
||||
{!isBgTextAccessible && (
|
||||
<Alert status="warning" mt={4} borderRadius="md">
|
||||
<AlertIcon />
|
||||
Slabý kontrast textu vůči pozadí (poměr {bgTextContrast?.toFixed(2)}). Pro čitelnost upravte barvy nebo
|
||||
<Button variant="link" colorScheme="blue" ml={2} onClick={autofixTextForBg}>automaticky opravit barvu textu</Button>.
|
||||
</Alert>
|
||||
)}
|
||||
{primaryTextContrast !== null && primaryTextContrast < 4.5 && (
|
||||
<Alert status="warning" mt={3} borderRadius="md">
|
||||
<AlertIcon />
|
||||
Text na primární barvě má nízký kontrast (poměr {primaryTextContrast.toFixed(2)}). Zvažte jinou barvu textu nebo primární barvy.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Divider my={6} />
|
||||
|
||||
<Heading as="h3" size="md" mb={2}>✍️ Písmo a typografie</Heading>
|
||||
<Text fontSize="sm" mb={3} color="gray.600">Vyberte vzhled písma pro váš web. Můžete kdykoliv změnit v administraci.</Text>
|
||||
<Box mb={4}>
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={3}>
|
||||
{FONT_PAIRINGS.map((font) => (
|
||||
<Box
|
||||
key={font.id}
|
||||
borderWidth={selectedFont === font.id ? '2px' : '1px'}
|
||||
borderColor={selectedFont === font.id ? 'blue.400' : 'gray.200'}
|
||||
borderRadius="md"
|
||||
p={3}
|
||||
cursor="pointer"
|
||||
onClick={() => setSelectedFont(font.id)}
|
||||
>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" fontWeight="medium">{font.name}</Text>
|
||||
<Badge colorScheme={getFontStyleColor(font.style)}>{font.style}</Badge>
|
||||
</HStack>
|
||||
<Text fontFamily={font.cssHeading} fontSize="lg" fontWeight="bold">Nadpis</Text>
|
||||
<Text fontFamily={font.cssBody} fontSize="sm">Text běžného odstavce</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Text fontSize="xs" color="gray.500" mt={2}>Zobrazeno {FONT_PAIRINGS.length} dostupných stylů písma.</Text>
|
||||
</Box>
|
||||
|
||||
<Divider my={6} />
|
||||
|
||||
<Heading as="h3" size="md" mb={2}>📍 GPS poloha a mapa</Heading>
|
||||
<Text fontSize="sm" mb={4} color="gray.600">Nastavte polohu vašeho stadionu. Můžete vložit odkaz z mapy, nebo zadat souřadnice ručně.</Text>
|
||||
|
||||
<Box mb={4}>
|
||||
<MapLinkImporter
|
||||
currentLatitude={typeof gpsLat === 'number' ? gpsLat : undefined}
|
||||
currentLongitude={typeof gpsLng === 'number' ? gpsLng : undefined}
|
||||
currentZoom={15}
|
||||
mapStyle={mapStyle}
|
||||
onMapStyleChange={setMapStyle}
|
||||
clubPrimaryColor={primaryColor}
|
||||
clubSecondaryColor={accentColor}
|
||||
clubName={clubName || 'Váš klub'}
|
||||
onImport={(coords: MapCoordinates) => {
|
||||
setGpsLat(coords.latitude);
|
||||
setGpsLng(coords.longitude);
|
||||
// Auto-fill address fields if available from geocoding
|
||||
if (coords.street) setContactStreet(coords.street);
|
||||
if (coords.city) setContactCity(coords.city);
|
||||
if (coords.zip) setContactPostalCode(coords.zip);
|
||||
if (coords.country) setContactCountry(coords.country);
|
||||
toast({
|
||||
title: 'Poloha importována',
|
||||
description: coords.city ? `GPS souřadnice a adresa (${coords.city}) načteny` : 'GPS souřadnice načteny',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider my={6} />
|
||||
|
||||
<Heading as="h3" size="md" mb={2}>🎨 Styl mapy</Heading>
|
||||
<Text fontSize="sm" mb={4} color="gray.600">Vyberte vzhled mapy, který nejlépe pasuje k barvám vašeho klubu.</Text>
|
||||
<Box mb={4}>
|
||||
<MapStyleSelector
|
||||
value={mapStyle}
|
||||
onChange={setMapStyle}
|
||||
clubPrimaryColor={primaryColor}
|
||||
clubSecondaryColor={accentColor}
|
||||
showPreview={true}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider my={6} />
|
||||
|
||||
<Heading as="h3" size="md" mb={2}>📧 Kontaktní údaje</Heading>
|
||||
<Text fontSize="sm" mb={3} color="gray.600">Tyto údaje se automaticky vyplní při importu z mapy. Můžete je upravit nebo doplnit ručně.</Text>
|
||||
<SimpleGrid columns={[1, 1, 2]} spacing={4} mb={4}>
|
||||
<FormControl>
|
||||
<FormLabel>Adresa (ulice a číslo)</FormLabel>
|
||||
<Input placeholder="Hlavní 123" value={contactStreet} onChange={(e) => setContactStreet(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Město</FormLabel>
|
||||
<Input placeholder="Krnov" value={contactCity} onChange={(e) => setContactCity(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>PSČ</FormLabel>
|
||||
<Input placeholder="794 01" value={contactPostalCode} onChange={(e) => setContactPostalCode(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Země</FormLabel>
|
||||
<Input placeholder="Česká republika" value={contactCountry} onChange={(e) => setContactCountry(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<Input type="tel" placeholder="+420 123 456 789" value={contactPhone} onChange={(e) => setContactPhone(e.target.value)} />
|
||||
<FormHelperText>Hlavní kontaktní telefon klubu</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>E-mail</FormLabel>
|
||||
<Input type="email" placeholder="kontakt@klub.cz" value={contactEmail} onChange={(e) => setContactEmail(e.target.value)} />
|
||||
<FormHelperText>Hlavní kontaktní e-mail klubu</FormHelperText>
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
|
||||
<Divider my={6} />
|
||||
|
||||
<Heading as="h3" size="md" mb={4}>🔒 Zabezpečení a SMTP</Heading>
|
||||
<SimpleGrid columns={[1, 1, 2]} spacing={6}>
|
||||
<FormControl>
|
||||
<FormLabel>JWT tajemství</FormLabel>
|
||||
<Input value={jwtSecret} onChange={(e) => setJwtSecret(e.target.value)} placeholder="Ponechte prázdné pro stávající hodnotu" />
|
||||
<Button mt={2} size="sm" onClick={() => setJwtSecret(generateJwtSecret())}>Vygenerovat bezpečné tajemství</Button>
|
||||
</FormControl>
|
||||
<Box>
|
||||
<FormControl mb={3}>
|
||||
<FormLabel>SMTP hostitel</FormLabel>
|
||||
<Input value={smtpHost} onChange={(e) => setSmtpHost(e.target.value)} placeholder="smtp.example.com" />
|
||||
</FormControl>
|
||||
<FormControl mb={3}>
|
||||
<FormLabel>SMTP port</FormLabel>
|
||||
<Input type="number" value={smtpPort} onChange={(e) => setSmtpPort(e.target.value ? Number(e.target.value) : '')} placeholder="587" />
|
||||
</FormControl>
|
||||
<FormControl mb={3}>
|
||||
<FormLabel>SMTP uživatelské jméno</FormLabel>
|
||||
<Input value={smtpUser} onChange={(e) => setSmtpUser(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl mb={3}>
|
||||
<FormLabel>SMTP heslo</FormLabel>
|
||||
<InputGroup>
|
||||
<Input type={showSmtpPass ? 'text' : 'password'} value={smtpPass} onChange={(e) => setSmtpPass(e.target.value)} />
|
||||
<InputRightElement width="4.5rem">
|
||||
<Button h="1.75rem" size="sm" onClick={() => setShowSmtpPass((s) => !s)}>{showSmtpPass ? 'Skrýt' : 'Zobrazit'}</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
<FormControl mb={3}>
|
||||
<FormLabel>Jméno odesílatele</FormLabel>
|
||||
<Input value={smtpFromName} onChange={(e) => setSmtpFromName(e.target.value)} placeholder="Název klubu (např. Fotbal Club)" />
|
||||
<FormHelperText>Jako adresu použijeme váš SMTP username (e‑mail). Zde vyplňte pouze zobrazované jméno.</FormHelperText>
|
||||
</FormControl>
|
||||
<Checkbox isChecked={smtpTLS} onChange={(e) => setSmtpTLS(e.target.checked)}>Použít TLS</Checkbox>
|
||||
<HStack mt={3} spacing={3}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const host = smtpHost.trim();
|
||||
const port = typeof smtpPort === 'number' ? smtpPort : 0;
|
||||
if (!host || !port) {
|
||||
toast({ title: 'Zadejte SMTP host a port', status: 'warning' });
|
||||
return;
|
||||
}
|
||||
setTestingSMTP(true);
|
||||
try {
|
||||
// Compose From as "Name <username>" for validation, if possible
|
||||
const composedFrom = smtpFromName && smtpUser ? `${smtpFromName} <${smtpUser}>` : (smtpFromName || undefined);
|
||||
const res = await validateSMTP({
|
||||
host,
|
||||
port,
|
||||
username: smtpUser || undefined,
|
||||
password: smtpPass || undefined,
|
||||
from: composedFrom,
|
||||
use_tls: smtpTLS,
|
||||
});
|
||||
if (res.ok) {
|
||||
toast({ title: 'SMTP ověřeno', description: 'Připojení a případná autentizace proběhly v pořádku.', status: 'success' });
|
||||
} else {
|
||||
toast({ title: 'SMTP selhalo', description: res.error || 'Zkontrolujte prosím údaje (host, port, šifrování, jméno/heslo).', status: 'error', duration: 7000 });
|
||||
}
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data?.error || e?.message || 'Neznámá chyba';
|
||||
toast({ title: 'SMTP selhalo', description: msg, status: 'error', duration: 7000 });
|
||||
} finally {
|
||||
setTestingSMTP(false);
|
||||
}
|
||||
}}
|
||||
isLoading={testingSMTP}
|
||||
>Otestovat SMTP</Button>
|
||||
<Tooltip label="Pro port 465 použijte implicitní SSL (ponechte zaškrtnuté). Pro 587 obvykle STARTTLS (také zaškrtnout).">
|
||||
<InfoOutlineIcon />
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
<Alert status="info" mt={4} borderRadius="md">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<Text fontWeight="medium">Před dokončením nastavení otestujte SMTP připojení.</Text>
|
||||
<Text fontSize="sm" mt={1}>Nejčastější chyba: nesprávné heslo. Ujistěte se, že heslo je zkopírováno přesně bez mezer.</Text>
|
||||
</Box>
|
||||
</Alert>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button type="submit" colorScheme="blue" mt={8} isLoading={submitting} loadingText="Ukládám…">Dokončit nastavení</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Lightweight preview of overall site style during setup
|
||||
const SetupStylePreview: React.FC<{ styleKey: 'unified' | 'magazine' | 'pro' | 'edge' }> = ({ styleKey }) => {
|
||||
// Try to render a screenshot if present in /dist/img
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const fileMap: Record<string, string> = {
|
||||
unified: 'style-unified.png',
|
||||
magazine: 'style-magazine.png',
|
||||
pro: 'style-pro.png',
|
||||
edge: 'style-edge.png',
|
||||
};
|
||||
const imgSrc = assetUrl(`/dist/img/${fileMap[styleKey] || fileMap.unified}`);
|
||||
|
||||
if (!imgError && imgSrc) {
|
||||
return (
|
||||
<Box borderWidth="1px" borderRadius="md" overflow="hidden">
|
||||
<Image src={imgSrc} alt={`Náhled stylu: ${styleKey}`} onError={() => setImgError(true)} width="100%" objectFit="cover" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (styleKey === 'magazine') {
|
||||
return (
|
||||
<Box className="magazine" borderRadius="md" overflow="hidden" borderWidth="1px" borderColor="gray.200">
|
||||
<Box className="mag-bars" />
|
||||
<Box p={3}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="bold">Magazínová hlavička</Text>
|
||||
<Text fontSize="sm" color="gray.500">2 barvy klubu</Text>
|
||||
</HStack>
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={2} mt={3}>
|
||||
<Box h="100px" bg="gray.100" borderRadius="md" />
|
||||
<Box h="48px" bg="gray.100" borderRadius="md" />
|
||||
<Box h="48px" bg="gray.100" borderRadius="md" />
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (styleKey === 'pro') {
|
||||
return (
|
||||
<Box className="pro" borderRadius="md" overflow="hidden" borderWidth="1px" borderColor="gray.200">
|
||||
<Box className="pro-hero" height="120px" position="relative">
|
||||
<Box position="absolute" inset={0} bg="gray.300" />
|
||||
<Box position="absolute" inset={0} display="flex" alignItems="center" justifyContent="center">
|
||||
<Text fontWeight="bold" color="white">Fullscreen Hero (náhled)</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box p={3}>
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={3}>
|
||||
<Box borderWidth="1px" borderRadius="md" p={3}>Zápasy</Box>
|
||||
<Box borderWidth="1px" borderRadius="md" p={3}>Aktuality</Box>
|
||||
<Box borderWidth="1px" borderRadius="md" p={3}>Vstupenka</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (styleKey === 'edge') {
|
||||
return (
|
||||
<Box borderRadius="md" overflow="hidden" borderWidth="1px" borderColor="gray.200" p={3}>
|
||||
<Text fontWeight="bold" mb={2}>Edge: Full‑width minimal</Text>
|
||||
<Box h="80px" bg="gray.200" borderRadius="md" mb={2} />
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={2}>
|
||||
<Box h="48px" bg="gray.100" borderRadius="md" />
|
||||
<Box h="48px" bg="gray.100" borderRadius="md" />
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box borderWidth="1px" borderRadius="md" p={3}>
|
||||
<Text fontSize="sm" color="gray.600">Stávající styl (Unified): Grid hero, sekce Nejbližší zápas, tři sloupce a sponzoři.</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupPage;
|
||||
Reference in New Issue
Block a user