mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
946 lines
43 KiB
TypeScript
946 lines
43 KiB
TypeScript
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, applyFontPairing, getFontStyleColor } from '../config/fonts';
|
||
import MapLinkImporter from '../components/admin/MapLinkImporter';
|
||
import MapStyleSelector from '../components/admin/MapStyleSelector';
|
||
import { MapCoordinates } from '../utils/mapUrlParser';
|
||
import { fetchLogoFromLogoAPI } from '../utils/sportLogosAPI';
|
||
|
||
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 logoapi URL, use it directly (no proxy needed)
|
||
if (u.includes('logoapi.sportcreative.eu')) return u;
|
||
// 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 and apply selected font for preview
|
||
useEffect(() => {
|
||
const pairing = FONT_PAIRINGS.find((f) => f.id === selectedFont);
|
||
if (pairing) {
|
||
applyFontPairing(pairing);
|
||
}
|
||
}, [selectedFont]);
|
||
|
||
// Auto-fill SMTP username from contact email
|
||
useEffect(() => {
|
||
if (contactEmail && !smtpUser) {
|
||
setSmtpUser(contactEmail);
|
||
}
|
||
}, [contactEmail, smtpUser]);
|
||
|
||
const handleSelectClub = async (item: SearchResult) => {
|
||
const clubIdValue = item.club_id || '';
|
||
setClubId(clubIdValue);
|
||
setClubType(item.club_type || 'football');
|
||
setClubName(item.name || '');
|
||
setClubUrl(item.url || '');
|
||
setClubQuery(item.name || '');
|
||
|
||
// Try to fetch logo from logoapi first, fallback to FACR logo
|
||
let logoUrl = '';
|
||
if (clubIdValue) {
|
||
const logoApiUrl = await fetchLogoFromLogoAPI(clubIdValue, item.name);
|
||
if (logoApiUrl) {
|
||
logoUrl = logoApiUrl;
|
||
}
|
||
}
|
||
// Fallback to FACR logo if logoapi doesn't have it
|
||
if (!logoUrl && item.logo_url) {
|
||
logoUrl = item.logo_url;
|
||
}
|
||
setClubLogoUrl(logoUrl);
|
||
|
||
// 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 (logoUrl) {
|
||
extractPalette(logoUrl, 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);
|
||
|
||
// Also upload to logoapi if we have a club ID
|
||
if (clubId) {
|
||
try {
|
||
const logoFd = new FormData();
|
||
logoFd.append('logo', f);
|
||
const logoApiRes = await fetch(`https://logoapi.sportcreative.eu/logos/${clubId}`, {
|
||
method: 'POST',
|
||
body: logoFd,
|
||
});
|
||
if (logoApiRes.ok) {
|
||
toast({ title: 'Logo nahráno', description: 'Logo bylo nahráno na logoapi i lokálně', status: 'success', duration: 3000 });
|
||
}
|
||
} catch (logoApiErr) {
|
||
console.warn('Failed to upload to logoapi:', logoApiErr);
|
||
// Don't fail the whole upload if logoapi fails
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
};
|
||
|
||
// Redirect if setup not required
|
||
useEffect(() => {
|
||
if (!loading && !requiresSetup) {
|
||
navigate('/login', { replace: true });
|
||
}
|
||
}, [loading, requiresSetup, navigate]);
|
||
|
||
if (loading) return <Box p={8}>Načítání…</Box>;
|
||
if (!requiresSetup) {
|
||
return null;
|
||
}
|
||
|
||
// Get selected font pairing for live preview
|
||
const selectedFontPairing = FONT_PAIRINGS.find((f) => f.id === selectedFont);
|
||
const fontHeading = selectedFontPairing?.cssHeading || 'inherit';
|
||
const fontBody = selectedFontPairing?.cssBody || 'inherit';
|
||
|
||
return (
|
||
<Box minH="100vh" bg="gray.50" display="flex" alignItems="center" justifyContent="center" px={8} py={8} fontFamily={fontBody}>
|
||
<Box as="form" onSubmit={handleSubmit} w="100%" maxW="3xl" p={8} bg={bg} borderRadius="xl" boxShadow="lg" borderWidth="1px" borderColor={borderCol} fontFamily={fontBody}>
|
||
<VStack spacing={3} mb={6} align="stretch">
|
||
<Heading size="xl" fontFamily={fontHeading}>🚀 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} fontFamily={fontHeading}>🔐 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} fontFamily={fontHeading}>⚽ 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.filter((r) => r.name && r.name.trim() !== '').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} fontFamily={fontHeading}>🎨 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, 2]} spacing={6}>
|
||
<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} fontFamily={fontHeading}>📱 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>
|
||
|
||
<Divider my={6} />
|
||
|
||
<Heading as="h3" size="md" mb={2} fontFamily={fontHeading}>✍️ Písmo a typografie</Heading>
|
||
<Text fontSize="sm" mb={3} color="gray.600">Vyberte vzhled písma pro váš web. Náhled se aplikuje okamžitě na celou stránku.</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} fontFamily={fontHeading}>📍 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ě. Vyberte také styl mapy.</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} fontFamily={fontHeading}>📧 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} fontFamily={fontHeading}>🔒 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;
|