This commit is contained in:
Tomáš Dvořák
2025-10-16 17:10:13 +02:00
parent f5e7be92c7
commit 35d0954afd
84 changed files with 9571 additions and 4668 deletions
+98 -72
View File
@@ -17,6 +17,7 @@ import { FONT_PAIRINGS, loadGoogleFont, getFontStyleColor } from '../config/font
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);
@@ -41,6 +42,8 @@ const SetupPage: React.FC = () => {
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
@@ -170,13 +173,28 @@ const SetupPage: React.FC = () => {
}
}, [selectedFont]);
const handleSelectClub = (item: SearchResult) => {
setClubId(item.club_id || '');
const handleSelectClub = async (item: SearchResult) => {
const clubIdValue = item.club_id || '';
setClubId(clubIdValue);
setClubType(item.club_type || 'football');
setClubName(item.name || '');
setClubLogoUrl(item.logo_url || '');
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);
@@ -188,8 +206,8 @@ const SetupPage: React.FC = () => {
}
} catch {}
// Try to extract colors
if (item.logo_url) {
extractPalette(item.logo_url, 5)
if (logoUrl) {
extractPalette(logoUrl, 5)
.then((colors) => {
if (!colors || colors.length === 0) return;
const presets = generateThemeCandidates(colors);
@@ -304,8 +322,8 @@ const SetupPage: React.FC = () => {
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`;
// 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();
@@ -315,6 +333,25 @@ const SetupPage: React.FC = () => {
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) {
@@ -376,17 +413,28 @@ const SetupPage: React.FC = () => {
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) {
navigate('/login', { replace: true });
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}>
<Box as="form" onSubmit={handleSubmit} w="100%" maxW="3xl" p={8} bg={bg} borderRadius="xl" boxShadow="lg" borderWidth="1px" borderColor={borderCol}>
<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">🚀 Vítejte v nastavení vašeho webu!</Heading>
<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>
@@ -401,7 +449,7 @@ const SetupPage: React.FC = () => {
<SimpleGrid columns={[1, 1, 2]} spacing={6}>
<Box>
<Heading as="h3" size="md" mb={4}>🔐 Administrátorský účet</Heading>
<Heading as="h3" size="md" mb={4} fontFamily={fontHeading}>🔐 Administrátorský účet</Heading>
<VStack align="stretch" spacing={4}>
<FormControl isRequired>
<FormLabel>Email administrátora</FormLabel>
@@ -439,7 +487,7 @@ const SetupPage: React.FC = () => {
</Box>
<Box>
<Heading as="h3" size="md" mb={4}> Informace o klubu</Heading>
<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>
@@ -452,7 +500,7 @@ const SetupPage: React.FC = () => {
{clubQuery && searchResults?.length > 0 && (
<Box mt={2} borderWidth="1px" borderRadius="md" maxH="240px" overflowY="auto">
<List spacing={0}>
{searchResults.slice(0, 8).map((r) => (
{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' }}
@@ -522,33 +570,7 @@ const SetupPage: React.FC = () => {
<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>
<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 */}
@@ -571,17 +593,7 @@ const SetupPage: React.FC = () => {
<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 (Fullwidth minimal)</option>
</Select>
<FormHelperText>Zvolte výchozí vzhled. Lze později změnit v administraci.</FormHelperText>
</FormControl>
<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>
@@ -632,8 +644,36 @@ const SetupPage: React.FC = () => {
<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>
<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) => (
@@ -662,8 +702,8 @@ const SetupPage: React.FC = () => {
<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>
<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
@@ -695,21 +735,7 @@ const SetupPage: React.FC = () => {
<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>
<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>
@@ -742,7 +768,7 @@ const SetupPage: React.FC = () => {
<Divider my={6} />
<Heading as="h3" size="md" mb={4}>🔒 Zabezpečení a SMTP</Heading>
<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>