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([]); const [selectedPreset, setSelectedPreset] = useState(null); // Typography/Font const [selectedFont, setSelectedFont] = useState('inter-inter'); // GPS Location const [mapUrl, setMapUrl] = useState(''); const [gpsLat, setGpsLat] = useState(''); const [gpsLng, setGpsLng] = useState(''); const [mapStyle, setMapStyle] = useState('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(''); 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 " 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 Načítání…; 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 ( 🚀 Vítejte v nastavení vašeho webu! Nastavte základní informace o vašem klubu. Můžete vše vyplnit nyní, nebo některé údaje doplnit později v administraci. 💡 Tip: Vyhledejte váš klub v databázi FAČR Logo, barvy a základní údaje se doplní automaticky. 🔐 Administrátorský účet E‑mail administrátora setAdminEmail(e.target.value)} placeholder="admin@example.com" /> Heslo administrátora setAdminPassword(sanitizePassword(e.target.value))} placeholder="Minimálně 8 znaků, bez mezer" minLength={8} /> Bez mezer. 8–128 znaků. Použijte písmena, číslice a speciální znaky. Jméno setFirstName(e.target.value)} /> Příjmení setLastName(e.target.value)} /> ⚽ Informace o klubu Hledat klub (FAČR) setClubQuery(e.target.value)} placeholder="Hledejte podle názvu klubu" /> {searchLoading ? : null} {clubQuery && searchResults?.length > 0 && ( {searchResults.filter((r) => r.name && r.name.trim() !== '').slice(0, 8).map((r) => ( handleSelectClub(r)} > {r.logo_url ? {r.name} : null} {r.name} {r.club_type} ))} )} Typ klubu setClubType(e.target.value)} placeholder="football nebo futsal" /> ID klubu setClubId(e.target.value)} placeholder="FAČR ID klubu" /> Název klubu setClubName(e.target.value)} /> URL loga klubu setClubLogoUrl(e.target.value)} /> {uploadingLogo ? Nahrávám… : null} URL klubu setClubUrl(e.target.value)} /> Náhled loga Logo preview {/* removed overall style preview per request */} 🎨 Barvy a vzhled webu Automaticky z loga (lze upravit). Vyberte jednu z předloh nebo barvy ručně dolaďte. {/* Preset selector */} {themePresets.length > 0 && ( Předlohy z loga {themePresets.map((p, i) => ( applyPreset(i)}> {/* primary */} {p.name} Primární / Sekundární / Akcent ))} )} Primární startTransition(() => setPrimaryColor(e.target.value))} /> Používá se na hlavních prvcích. Sekundární startTransition(() => setSecondaryColor(e.target.value))} /> Podpůrné zvýraznění. Akcent startTransition(() => setAccentColor(e.target.value))} /> Menší prvky a upozornění. Pozadí startTransition(() => setBackgroundColor(e.target.value))} /> Barva ploch stránky. Text startTransition(() => setTextColor(e.target.value))} /> Základní barva textu. {/* Contrast warnings */} {!isBgTextAccessible && ( Slabý kontrast textu vůči pozadí (poměr {bgTextContrast?.toFixed(2)}). Pro čitelnost upravte barvy nebo . )} {primaryTextContrast !== null && primaryTextContrast < 4.5 && ( Text na primární barvě má nízký kontrast (poměr {primaryTextContrast.toFixed(2)}). Zvažte jinou barvu textu nebo primární barvy. )} 📱 Sociální sítě a fotogalerie Zadejte odkazy na profily klubu a volitelně na fotogalerii. Lze později upravit v administraci. Facebook URL setFacebookUrl(e.target.value)} /> Instagram URL setInstagramUrl(e.target.value)} /> YouTube URL setYoutubeUrl(e.target.value)} /> URL fotogalerie setGalleryUrl(e.target.value)} /> Můžete použít libovolný web (SmugMug, Flickr, Google Photos, Zonerama...). Popisek odkazu fotogalerie setGalleryLabel(e.target.value)} /> ✍️ Písmo a typografie Vyberte vzhled písma pro váš web. Náhled se aplikuje okamžitě na celou stránku. {FONT_PAIRINGS.map((font) => ( setSelectedFont(font.id)} > {font.name} {font.style} Nadpis Text běžného odstavce ))} Zobrazeno {FONT_PAIRINGS.length} dostupných stylů písma. 📍 GPS poloha a mapa Nastavte polohu vašeho stadionu. Můžete vložit odkaz z mapy, nebo zadat souřadnice ručně. Vyberte také styl mapy. { 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, }); }} /> 📧 Kontaktní údaje Tyto údaje se automaticky vyplní při importu z mapy. Můžete je upravit nebo doplnit ručně. Adresa (ulice a číslo) setContactStreet(e.target.value)} /> Město setContactCity(e.target.value)} /> PSČ setContactPostalCode(e.target.value)} /> Země setContactCountry(e.target.value)} /> Telefon setContactPhone(e.target.value)} /> Hlavní kontaktní telefon klubu E-mail setContactEmail(e.target.value)} /> Hlavní kontaktní e-mail klubu 🔒 Zabezpečení a SMTP JWT tajemství setJwtSecret(e.target.value)} placeholder="Ponechte prázdné pro stávající hodnotu" /> SMTP hostitel setSmtpHost(e.target.value)} placeholder="smtp.example.com" /> SMTP port setSmtpPort(e.target.value ? Number(e.target.value) : '')} placeholder="587" /> SMTP uživatelské jméno setSmtpUser(e.target.value)} /> SMTP heslo setSmtpPass(e.target.value)} /> Jméno odesílatele setSmtpFromName(e.target.value)} placeholder="Název klubu (např. Fotbal Club)" /> Jako adresu použijeme váš SMTP username (e‑mail). Zde vyplňte pouze zobrazované jméno. setSmtpTLS(e.target.checked)}>Použít TLS Před dokončením nastavení otestujte SMTP připojení. Nejčastější chyba: nesprávné heslo. Ujistěte se, že heslo je zkopírováno přesně bez mezer. ); }; // 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 = { 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 ( {`Náhled setImgError(true)} width="100%" objectFit="cover" /> ); } if (styleKey === 'magazine') { return ( Magazínová hlavička 2 barvy klubu ); } if (styleKey === 'pro') { return ( Fullscreen Hero (náhled) Zápasy Aktuality Vstupenka ); } if (styleKey === 'edge') { return ( Edge: Full‑width minimal ); } return ( Stávající styl (Unified): Grid hero, sekce Nejbližší zápas, tři sloupce a sponzoři. ); }; export default SetupPage;