This commit is contained in:
Tomas Dvorak
2025-12-02 13:52:21 +01:00
parent 025f5beef1
commit b27fe6fe0e
33 changed files with 5164 additions and 1043 deletions
+4 -193
View File
@@ -1,202 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20 transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Admin Header -->
<header class="border-b border-dark-border bg-dark-card">
<div class="container mx-auto px-6 py-8">
<h1 class="text-3xl font-bold gradient-text mb-2">Administrace</h1>
<p class="text-gray-400">Vyhledejte kluby a nahrajte jejich loga</p>
</div>
</header>
<main class="container mx-auto px-6 py-12">
<!-- Club Search Section -->
<section class="mb-12">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">🔍 Vyhledat Klub</h2>
<div class="relative mb-6">
<input
type="text"
id="clubSearch"
placeholder="Hledat české kluby (např. Sparta, Slavia)..."
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
</div>
<!-- Search Results -->
<div id="searchResults" class="space-y-3">
<!-- Výsledky naplněné JavaScriptem -->
</div>
</div>
</section>
<!-- Upload Section -->
<section id="uploadSection" class="hidden">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6"><span style="font-size: 30px; display: inline-block; vertical-align: middle; line-height: 1;">⬆️</span> Nahrát Logo</h2>
<form id="uploadForm" class="space-y-6">
<!-- Club UUID (Read-only) -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
UUID Klubu <span class="text-red-500">*</span>
</label>
<input
type="text"
id="clubUuid"
readonly
class="w-full bg-dark-bg/50 border border-dark-border rounded-lg px-4 py-3 text-gray-400 cursor-not-allowed"
>
</div>
<!-- Club Name (Optional) -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
Název Klubu <span class="text-gray-500 text-xs">(volitelné)</span>
</label>
<input
type="text"
id="clubName"
placeholder="AC Sparta Praha"
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
<p class="text-xs text-gray-500 mt-1">Volitelné: Pokud název neuvedete, doplníme jej automaticky dle FAČR (podle UUID)</p>
</div>
<!-- Club Type -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">Typ Klubu</label>
<select
id="clubType"
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
<option value="football">Fotbal</option>
<option value="futsal">Futsal</option>
</select>
</div>
<!-- Club Website with Search -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
Web Klubu
<button type="button" id="searchWebsite" class="ml-2 text-accent-blue hover:text-blue-400 text-xs">
🔍 Hledat Online
</button>
</label>
<input
type="url"
id="clubWebsite"
placeholder="https://www.sparta.cz"
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
<div id="websiteSearchResults" class="mt-2 hidden"></div>
</div>
<!-- File Upload Area -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
Soubor Loga <span class="text-red-500">*</span>
</label>
<!-- URL Upload -->
<div class="mb-3">
<input
type="url"
id="logoUrl"
placeholder="Nebo vložte URL obrázku (https://...)"
class="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
<button type="button" id="loadFromUrl" class="mt-2 px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-smooth text-sm">
📥 Načíst z URL
</button>
</div>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-dark-border"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-dark-card text-gray-400">nebo</span>
</div>
</div>
<div id="uploadArea" class="upload-area rounded-lg p-12 text-center cursor-pointer border-2 border-dashed border-dark-border hover:border-accent-blue transition-smooth mt-3">
<svg style="width: 75px;padding-top: 20px;" class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-lg mb-2">Přetáhněte logo sem nebo <span class="text-accent-blue font-semibold">procházet</span></p>
<p class="text-sm text-gray-500">SVG, PNG nebo PDF • Preferováno průhledné pozadí</p>
<p class="text-xs text-gray-600 mt-2">SVG a PDF soubory budou automaticky převedeny na PNG</p>
<input type="file" id="fileInput" accept=".svg,.png,.pdf" class="hidden" multiple>
</div>
<p class="text-xs text-gray-500 mt-2">💡 Můžete vybrat více souborů najednou pro nahrání variant</p>
</div>
<!-- Files Preview -->
<div id="filesPreviewArea" class="hidden">
<h3 class="text-lg font-semibold mb-3">Vybrané soubory</h3>
<div id="filesPreviewList" class="space-y-3">
<!-- Files will be listed here -->
</div>
</div>
<!-- Upload Button -->
<button
type="submit"
id="uploadSubmit"
class="w-full px-6 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth disabled:opacity-50 disabled:cursor-not-allowed text-lg"
>
Nahrát Logo
</button>
<!-- Requirements Notice -->
<div class="bg-red-900/20 border border-red-800 rounded-lg p-4 text-sm">
<p class="font-semibold text-red-400 mb-2">⚠️ Požadavky na nahrání:</p>
<ul class="list-disc list-inside space-y-1 text-red-300/80">
<li>Název klubu je volitelný (doplníme dle FAČR podle UUID)</li>
<li>UUID klubu musí být platné</li>
<li>Akceptovány pouze SVG, PNG a PDF soubory</li>
<li>Doporučeno průhledné pozadí</li>
</ul>
</div>
</form>
</div>
</section>
</main>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API | Administrace</p>
</div>
</footer>
<script type="module" src="/src/admin.js"></script>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/admin-main.tsx"></script>
</body>
</html>
+4 -414
View File
@@ -1,423 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Dokumentace - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Header -->
<header class="border-b border-dark-border bg-dark-card">
<div class="container mx-auto px-6 py-12">
<h1 class="text-4xl font-bold gradient-text mb-3">📚 API Dokumentace</h1>
<p class="text-xl text-gray-400">Kompletní referenční příručka pro České Kluby Loga API</p>
<div class="mt-6 flex gap-4 items-center flex-wrap">
<div>
<span class="text-sm text-gray-400 mr-2">Frontend:</span>
<code class="bg-dark-bg px-4 py-2 rounded text-accent-blue">http://localhost:3000</code>
</div>
<div>
<span class="text-sm text-gray-400 mr-2">Backend API:</span>
<code class="bg-dark-bg px-4 py-2 rounded text-accent-green">http://localhost:8080</code>
</div>
</div>
<p class="text-sm text-gray-400 mt-3">💡 Ve vývojovém prostředí používejte relativní cesty (např. <code class="text-accent-blue">/logos</code>), Vite proxy je přesměruje na backend</p>
</div>
</header>
<!-- Main Content -->
<main class="container mx-auto px-6 py-12">
<!-- Quick Start -->
<section class="mb-16">
<h2 class="text-3xl font-bold mb-6">🚀 Rychlý Start</h2>
<div class="bg-gradient-to-br from-accent-green/10 to-accent-blue/10 rounded-xl p-6 border-2 border-accent-green/30">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span class="text-2xl">⬆️</span>
Nahrání loga klubu - Základní příkaz
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/{club-uuid} \
-F "file=@logo.svg" \
-F "club_name=Název Klubu"</code></pre>
<div class="mt-4 space-y-2">
<p class="text-sm text-gray-300"><strong class="text-accent-green">Povinné:</strong> Club UUID v URL, soubor loga (SVG/PNG/PDF), název klubu</p>
<p class="text-sm text-gray-300"><strong class="text-accent-blue">Volitelné:</strong> club_type, club_website, club_city</p>
</div>
</div>
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mt-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span class="text-2xl">📥</span>
Stažení loga klubu
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm"># Přímo z backendu
curl http://localhost:8080/logos/{uuid}
# Přes frontend proxy
curl http://localhost:3000/api/logos/{uuid}</code></pre>
<p class="text-gray-400 mt-3 text-sm">Vrátí PNG obrázek loga (SVG jako fallback)</p>
</div>
</section>
<!-- Endpoints -->
<section class="mb-16">
<h2 class="text-3xl font-bold mb-6">📡 Endpointy</h2>
<!-- List Logos -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
<code class="text-lg">/logos</code>
</div>
<p class="text-gray-400 mb-4">Seznam všech nahraných log</p>
<div class="bg-dark-bg rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
<pre class="text-sm overflow-x-auto"><code>[
{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"has_svg": true,
"has_png": true,
"logo_url": "http://localhost:8080/logos/uuid-here",
"created_at": "2024-01-01T12:00:00Z"
}
]</code></pre>
</div>
</div>
<!-- Get Logo File -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
<code class="text-lg">/logos/:id</code>
</div>
<p class="text-gray-400 mb-4">Získání souboru loga (PNG preferováno, SVG jako fallback)</p>
<h4 class="text-sm font-semibold mb-2">Query Parameters (volitelné):</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<code class="text-sm">format</code> <span class="text-gray-500">string</span> - "png" nebo "svg"
</div>
<div class="bg-dark-bg rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
<p class="text-sm text-gray-400">Binární data obrázku (image/png nebo image/svg+xml)</p>
</div>
</div>
<!-- Get Logo Metadata -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">GET</span>
<code class="text-lg">/logos/:id/json</code>
</div>
<p class="text-gray-400 mb-4">Získání metadat loga ve formátu JSON</p>
<div class="bg-dark-bg rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200:</h4>
<pre class="text-sm overflow-x-auto"><code>{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"club_website": "https://sparta.cz",
"has_svg": true,
"has_png": true,
"primary_format": "png",
"logo_url": "http://localhost:8080/logos/uuid-here",
"logo_url_svg": "http://localhost:8080/logos/uuid-here?format=svg",
"logo_url_png": "http://localhost:8080/logos/uuid-here?format=png",
"file_size_svg": 12345,
"file_size_png": 54321,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}</code></pre>
</div>
</div>
<!-- Upload Logo -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6 border-2 border-accent-green/40">
<div class="flex items-center gap-3 mb-4">
<span class="px-3 py-1 bg-accent-green/20 text-accent-green rounded font-mono text-sm">POST</span>
<code class="text-lg">/logos/:id</code>
</div>
<p class="text-gray-400 mb-4">Nahrání nového loga klubu s kompletními daty (ID klubu, název, logo soubory)</p>
<h4 class="text-sm font-semibold mb-2">URL Parameters:</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<code class="text-sm">:id</code> <span class="text-red-400">*</span> <span class="text-gray-500">UUID</span> - Jedinečné ID klubu (např. <code class="text-xs">550e8400-e29b-41d4-a716-446655440000</code>)
</div>
<h4 class="text-sm font-semibold mb-2">Content-Type:</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<code class="text-sm">multipart/form-data</code>
</div>
<h4 class="text-sm font-semibold mb-2">Form Data (Povinné pole):</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div class="border-l-2 border-red-400 pl-3">
<code class="text-sm font-semibold text-red-400">file</code> <span class="text-red-400">*</span> <span class="text-gray-500">file (SVG nebo PNG)</span>
<p class="text-xs text-gray-500 mt-1">Soubor loga. Podporované formáty: SVG (doporučeno), PNG, PDF</p>
</div>
<div class="border-l-2 border-red-400 pl-3">
<code class="text-sm font-semibold text-red-400">club_name</code> <span class="text-red-400">*</span> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Název klubu (např. "AC Sparta Praha")</p>
</div>
</div>
<h4 class="text-sm font-semibold mb-2">Form Data (Volitelné):</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div class="border-l-2 border-blue-400 pl-3">
<code class="text-sm">club_type</code> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Typ klubu: <code>"football"</code> (výchozí) nebo <code>"futsal"</code></p>
</div>
<div class="border-l-2 border-blue-400 pl-3">
<code class="text-sm">club_website</code> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">URL webové stránky klubu (např. "https://sparta.cz")</p>
</div>
<div class="border-l-2 border-blue-400 pl-3">
<code class="text-sm">club_city</code> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Město klubu (např. "Praha")</p>
</div>
</div>
<div class="bg-dark-bg rounded-lg p-4 mb-4">
<h4 class="text-sm font-semibold text-gray-400 mb-2">Response 200 (Úspěch):</h4>
<pre class="text-sm overflow-x-auto"><code>{
"success": true,
"id": "550e8400-e29b-41d4-a716-446655440000",
"club_name": "AC Sparta Praha",
"has_svg": true,
"has_png": true,
"size_svg": 12543,
"size_png": 45210,
"message": "logo uploaded successfully"
}</code></pre>
</div>
<div class="bg-red-900/20 rounded-lg p-4 border border-red-600/30">
<h4 class="text-sm font-semibold text-red-400 mb-2">Response 400 (Chyba):</h4>
<pre class="text-sm overflow-x-auto"><code>{
"error": "club_name is required"
}</code></pre>
<p class="text-xs text-gray-400 mt-2">Možné chyby: <code>"no file provided"</code>, <code>"invalid UUID format"</code>, <code>"only .svg, .png and .pdf files are allowed"</code></p>
</div>
</div>
</section>
<!-- Examples -->
<section class="mb-16">
<h2 class="text-3xl font-bold mb-6">💡 Příklady Použití - Nahrání Loga</h2>
<!-- cURL Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>🔧</span> cURL (Terminal)
</h3>
<div class="space-y-4">
<div>
<h4 class="text-sm font-semibold mb-2 text-accent-green">Minimální nahrání (pouze povinná pole):</h4>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha"</code></pre>
</div>
<div>
<h4 class="text-sm font-semibold mb-2 text-accent-blue">Kompletní nahrání (všechna data):</h4>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha" \
-F "club_type=football" \
-F "club_website=https://sparta.cz" \
-F "club_city=Praha"</code></pre>
</div>
<div>
<h4 class="text-sm font-semibold mb-2 text-gray-400">Nahrání PNG místo SVG:</h4>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">curl -X POST http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.png" \
-F "club_name=AC Sparta Praha"</code></pre>
</div>
</div>
</div>
<!-- JavaScript Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>📜</span> JavaScript (Fetch API)
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">// Funkce pro nahrání loga s kompletními daty
async function uploadClubLogo(clubId, file, clubData) {
const formData = new FormData();
// Povinná pole
formData.append('file', file);
formData.append('club_name', clubData.name);
// Volitelná pole
if (clubData.type) formData.append('club_type', clubData.type);
if (clubData.website) formData.append('club_website', clubData.website);
if (clubData.city) formData.append('club_city', clubData.city);
const response = await fetch(`http://localhost:8080/logos/${clubId}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return await response.json();
}
// Použití s file input
const fileInput = document.getElementById('logoFile');
const clubId = '550e8400-e29b-41d4-a716-446655440000';
const result = await uploadClubLogo(clubId, fileInput.files[0], {
name: 'AC Sparta Praha',
type: 'football',
website: 'https://sparta.cz',
city: 'Praha'
});
console.log('Upload successful:', result);</code></pre>
</div>
<!-- Python Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>🐍</span> Python (requests)
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm">import requests
def upload_club_logo(club_id, file_path, club_name, **optional_data):
"""
Nahraje logo klubu s kompletními daty
Args:
club_id: UUID klubu
file_path: Cesta k souboru loga
club_name: Název klubu (povinný)
**optional_data: club_type, club_website, club_city
"""
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'club_name': club_name}
data.update(optional_data)
response = requests.post(
f"http://localhost:8080/logos/{club_id}",
files=files,
data=data
)
response.raise_for_status()
return response.json()
# Použití
result = upload_club_logo(
club_id='550e8400-e29b-41d4-a716-446655440000',
file_path='sparta_logo.svg',
club_name='AC Sparta Praha',
club_type='football',
club_website='https://sparta.cz',
club_city='Praha'
)
print(f"Upload úspěšný: {result['message']}")
print(f"Has SVG: {result['has_svg']}, Has PNG: {result['has_png']}")</code></pre>
</div>
<!-- PowerShell Example -->
<div class="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
<span>💻</span> PowerShell
</h3>
<pre class="bg-dark-bg rounded-lg p-4 overflow-x-auto"><code class="text-sm"># Nahrání loga s kompletními daty
$clubId = "550e8400-e29b-41d4-a716-446655440000"
$logoFile = "C:\logos\sparta_logo.svg"
$form = @{
file = Get-Item -Path $logoFile
club_name = "AC Sparta Praha"
club_type = "football"
club_website = "https://sparta.cz"
club_city = "Praha"
}
$result = Invoke-RestMethod `
-Uri "http://localhost:8080/logos/$clubId" `
-Method Post `
-Form $form
Write-Host "Upload úspěšný: $($result.message)" -ForegroundColor Green
Write-Host "Club: $($result.club_name)" -ForegroundColor Cyan</code></pre>
</div>
</section>
<!-- Error Codes -->
<section>
<h2 class="text-3xl font-bold mb-6">⚠️ Chybové Kódy</h2>
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<div class="space-y-4">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-green-600/20 text-green-400 rounded text-sm font-mono">200</span>
<div>
<h4 class="font-semibold">OK</h4>
<p class="text-gray-400 text-sm">Požadavek úspěšně dokončen</p>
</div>
</div>
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">400</span>
<div>
<h4 class="font-semibold">Bad Request</h4>
<p class="text-gray-400 text-sm">Neplatné parametry nebo chybějící povinná pole</p>
</div>
</div>
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">404</span>
<div>
<h4 class="font-semibold">Not Found</h4>
<p class="text-gray-400 text-sm">Logo nebo klub nenalezen</p>
</div>
</div>
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">500</span>
<div>
<h4 class="font-semibold">Internal Server Error</h4>
<p class="text-gray-400 text-sm">Interní chyba serveru</p>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API</p>
</div>
</footer>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/docs-main.tsx"></script>
</body>
</html>
+5 -174
View File
@@ -1,182 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🇨🇿 České Kluby Loga API</title>
<title>České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/logos.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Všechna Loga</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<header class="relative overflow-hidden border-b border-dark-border">
<div class="absolute inset-0 bg-gradient-to-br from-blue-600/10 to-green-600/10"></div>
<div class="container mx-auto px-6 py-20 relative z-10">
<div class="text-center hero-content max-w-4xl mx-auto">
<h1 class="text-5xl md:text-7xl font-bold mb-6">
<span class="gradient-text">České Kluby Loga CDN</span>
</h1>
<p class="text-xl text-gray-400 mb-8">
Vysoce kvalitní loga českých fotbalových a futsalových klubů s průhledným pozadím.
Založeno na UUID, API-first, připraveno pro produkci.
</p>
<div class="flex flex-wrap gap-4 justify-center">
<button id="browseBtn" class="px-8 py-4 bg-accent-blue rounded-lg font-semibold hover:bg-blue-600 transition-smooth text-lg">
🔍 Procházet Loga
</button>
<a href="/admin.html" class="px-8 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth text-lg">
⬆️ Nahrát Logo
</a>
</div>
</div>
</div>
</header>
<!-- Logo Gallery -->
<section class="container mx-auto px-6 py-16" id="logoGallery">
<div class="mb-8">
<h2 class="text-3xl font-bold mb-4">Dostupná Loga Klubů</h2>
<input
type="text"
id="gallerySearch"
placeholder="Filtrovat podle názvu klubu..."
class="w-full max-w-md bg-dark-card border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
</div>
<div id="logoGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-6">
<!-- Logos will be loaded here -->
</div>
<div id="loadingState" class="text-center py-16">
<div class="spinner mx-auto"></div>
<p class="mt-4 text-gray-400">Načítání log klubů...</p>
</div>
<div id="emptyState" class="text-center py-16 hidden">
<div class="text-6xl mb-4"></div>
<p class="text-xl text-gray-400 mb-4">Zatím nebyla nahrána žádná loga</p>
<a href="/admin.html" class="px-6 py-3 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth inline-block">
Nahrát První Logo
</a>
</div>
</section>
<!-- API Documentation Preview -->
<section class="bg-dark-card border-y border-dark-border py-16">
<div class="container mx-auto px-6">
<h2 class="text-3xl font-bold mb-8 text-center">Rychlá Referenční API</h2>
<div class="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos</p>
<p class="text-gray-400 text-sm">Zobrazit všechna dostupná loga</p>
</div>
</div>
</div>
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos/:id</p>
<p class="text-gray-400 text-sm">Získat logo podle UUID (PNG/SVG)</p>
</div>
</div>
</div>
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">GET</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos/:id/json</p>
<p class="text-gray-400 text-sm">Získat metadata loga</p>
</div>
</div>
</div>
<div class="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div class="flex items-start gap-4">
<span class="px-3 py-1 bg-accent-green/20 text-accent-green rounded-md text-sm font-mono">POST</span>
<div class="flex-1">
<p class="font-mono text-sm mb-2">/logos/:id</p>
<p class="text-gray-400 text-sm">Nahrát nové logo</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features -->
<section class="container mx-auto px-6 py-16">
<h2 class="text-3xl font-bold mb-12 text-center">✨ Funkce</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4"></div>
<h3 class="text-xl font-semibold mb-2">Integrace s FAČR</h3>
<p class="text-gray-400">Přímá integrace s oficiálním českým fotbalovým registrem</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🖼️</div>
<h3 class="text-xl font-semibold mb-2">SVG & PNG</h3>
<p class="text-gray-400">Nahrajte SVG, PNG se vygeneruje automaticky</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🔄</div>
<h3 class="text-xl font-semibold mb-2">Založeno na UUID</h3>
<p class="text-gray-400">Konzistentní identifikace napříč všemi platformami</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🌐</div>
<h3 class="text-xl font-semibold mb-2">Připraveno pro CDN</h3>
<p class="text-gray-400">Rychlé, cachovatelné, produkční API</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">📝</div>
<h3 class="text-xl font-semibold mb-2">Bohatá Metadata</h3>
<p class="text-gray-400">Název klubu, město, typ, web v ceně</p>
</div>
<div class="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<div class="text-3xl mb-4">🐳</div>
<h3 class="text-xl font-semibold mb-2">Připraveno pro Docker</h3>
<p class="text-gray-400">Nasazení jedním příkazem s Docker Compose</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API | Vytvořeno s ❤️ pro český fotbal</p>
<p class="text-sm mt-2">Poháněno FAČR Scraper API | Open Source MIT Licence</p>
</div>
</footer>
<script type="module" src="/src/home.js"></script>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4 -153
View File
@@ -1,162 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Detail Loga - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<!-- Navigation -->
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">🇨🇿 České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="container mx-auto px-6 py-12">
<!-- Loading State -->
<div id="loadingState" class="text-center py-12">
<div class="spinner mx-auto mb-4"></div>
<p class="text-gray-400">Načítání...</p>
</div>
<!-- Error State -->
<div id="errorState" class="hidden text-center py-12">
<svg class="mx-auto h-16 w-16 text-red-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h2 class="text-2xl font-bold mb-2">Logo nenalezeno</h2>
<p class="text-gray-400 mb-4">Logo s tímto UUID neexistuje</p>
<a href="/" class="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth inline-block">
Zpět na hlavní stránku
</a>
</div>
<!-- Logo Detail -->
<div id="logoDetail" class="hidden">
<!-- Header -->
<div class="flex items-start justify-between mb-8">
<div>
<h1 id="clubName" class="text-4xl font-bold gradient-text mb-2"></h1>
<p id="clubMeta" class="text-gray-400"></p>
</div>
<a id="editButton" href="/admin.html" class="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth">
✏️ Upravit
</a>
</div>
<!-- Logo Preview -->
<section class="mb-8">
<div class="bg-dark-card rounded-xl p-8 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">📷 Náhled Loga</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Light Background -->
<div class="bg-white rounded-lg p-8 flex items-center justify-center min-h-[300px]">
<img id="logoPreviewLight" src="" alt="Logo na světlém pozadí" class="max-w-full max-h-64 object-contain">
</div>
<!-- Dark Background -->
<div class="bg-gray-900 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
<img id="logoPreviewDark" src="" alt="Logo na tmavém pozadí" class="max-w-full max-h-64 object-contain">
</div>
</div>
</div>
</section>
<!-- Available Formats -->
<section class="mb-8">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">💾 Dostupné Formáty</h2>
<div id="formatsGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Formats will be populated here -->
</div>
</div>
</section>
<!-- Variants -->
<section class="mb-8" id="variantsSection">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">🎨 Varianty Loga</h2>
<div id="variantsGrid" class="space-y-4">
<!-- Variants will be populated here -->
</div>
</div>
</section>
<!-- Metadata -->
<section class="mb-8">
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">️ Informace</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">UUID</h3>
<p id="logoUuid" class="font-mono text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">Typ Klubu</h3>
<p id="clubType" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">Webová Stránka</h3>
<p id="clubWebsite" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">Datum Nahrání</h3>
<p id="uploadDate" class="text-sm bg-dark-bg rounded px-3 py-2"></p>
</div>
</div>
</div>
</section>
<!-- API Usage -->
<section>
<div class="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 class="text-2xl font-bold mb-6">🔗 Použití API</h2>
<div class="space-y-4">
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">GET Logo (PNG preferováno)</h3>
<div class="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code id="apiUrlDefault"></code>
<button onclick="copyToClipboard('apiUrlDefault')" class="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth">
Kopírovat
</button>
</div>
</div>
<div>
<h3 class="text-sm font-medium text-gray-400 mb-2">GET Logo s Metadaty (JSON)</h3>
<div class="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code id="apiUrlJson"></code>
<button onclick="copyToClipboard('apiUrlJson')" class="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth">
Kopírovat
</button>
</div>
</div>
</div>
</div>
</section>
</div>
</main>
<!-- Footer -->
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API</p>
</div>
</footer>
<script type="module" src="/src/logo.js"></script>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/logo-main.tsx"></script>
</body>
</html>
+4 -58
View File
@@ -1,67 +1,13 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Všechna Loga - České Kluby Loga API</title>
<link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-dark-bg text-white min-h-screen">
<nav class="border-b border-dark-border bg-dark-card/50 backdrop-blur-sm sticky top-0 z-50">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<a href="/" class="text-2xl font-bold gradient-text">České Kluby Loga</a>
<div class="flex gap-4">
<a href="/" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Domů</a>
<a href="/logos.html" class="nav-link px-4 py-2 rounded-lg bg-accent-blue/20 transition-smooth">Všechna Loga</a>
<a href="/api-docs.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">API Docs</a>
<a href="/admin.html" class="nav-link px-4 py-2 rounded-lg hover:bg-dark-border transition-smooth">Admin</a>
</div>
</div>
</div>
</nav>
<header class="border-b border-dark-border bg-dark-card">
<div class="container mx-auto px-6 py-8">
<h1 class="text-3xl font-bold gradient-text mb-2">Všechna Loga</h1>
<p class="text-gray-400">Procházejte všechna dostupná loga, vyhledávejte a spravujte</p>
</div>
</header>
<main class="container mx-auto px-6 py-12">
<div class="mb-6 flex flex-col md:flex-row gap-3 md:items-center">
<input
type="text"
id="allLogoSearch"
placeholder="Hledat mezi všemi logy..."
class="w-full md:max-w-lg bg-dark-card border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
<div class="text-xs text-gray-500">20 log na stránku • řazeno: nejnovější</div>
</div>
<div id="allLoading" class="text-center py-12">
<div class="spinner mx-auto"></div>
<p class="mt-4 text-gray-400">Načítání log...</p>
</div>
<div id="allEmpty" class="text-center py-16 hidden">
<div class="text-6xl mb-4"></div>
<p class="text-xl text-gray-400 mb-4">Žádná loga nenalezena</p>
</div>
<div id="allLogoGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-6"></div>
<div class="text-center mt-10">
<button id="loadMoreBtn" class="px-6 py-3 bg-dark-card border border-dark-border rounded-lg hover:bg-dark-border transition-smooth hidden">Načíst další</button>
</div>
</main>
<footer class="border-t border-dark-border mt-20">
<div class="container mx-auto px-6 py-8 text-center text-gray-400">
<p>🇨🇿 České Kluby Loga API</p>
</div>
</footer>
<script type="module" src="/src/logos.js"></script>
<body class="bg-dark-bg min-h-screen">
<div id="root"></div>
<script type="module" src="/src/logos-main.tsx"></script>
</body>
</html>
+240
View File
@@ -17,6 +17,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
@@ -140,6 +157,40 @@
"node": ">=14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz",
"integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz",
"integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
@@ -168,6 +219,91 @@
"win32"
]
},
"node_modules/@swc/core": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz",
"integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.3",
"@swc/core-darwin-x64": "1.15.3",
"@swc/core-linux-arm-gnueabihf": "1.15.3",
"@swc/core-linux-arm64-gnu": "1.15.3",
"@swc/core-linux-arm64-musl": "1.15.3",
"@swc/core-linux-x64-gnu": "1.15.3",
"@swc/core-linux-x64-musl": "1.15.3",
"@swc/core-win32-arm64-msvc": "1.15.3",
"@swc/core-win32-ia32-msvc": "1.15.3",
"@swc/core-win32-x64-msvc": "1.15.3"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz",
"integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz",
"integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true
},
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"dev": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -175,6 +311,44 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
"integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
"dev": true,
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.27",
"@swc/core": "^1.12.11"
},
"peerDependencies": {
"vite": "^4 || ^5 || ^6 || ^7"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -481,6 +655,12 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -817,6 +997,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -837,6 +1022,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -1230,6 +1426,29 @@
],
"license": "MIT"
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -1351,6 +1570,14 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1618,6 +1845,19 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+330 -1
View File
@@ -8,12 +8,18 @@
"name": "czech-clubs-logos-frontend",
"version": "1.0.0",
"dependencies": {
"gsap": "^3.12.5"
"gsap": "^3.12.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.0",
"vite": "^5.2.11"
}
},
@@ -527,6 +533,12 @@
"node": ">=14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
@@ -835,6 +847,219 @@
"win32"
]
},
"node_modules/@swc/core": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz",
"integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.15.3",
"@swc/core-darwin-x64": "1.15.3",
"@swc/core-linux-arm-gnueabihf": "1.15.3",
"@swc/core-linux-arm64-gnu": "1.15.3",
"@swc/core-linux-arm64-musl": "1.15.3",
"@swc/core-linux-x64-gnu": "1.15.3",
"@swc/core-linux-x64-musl": "1.15.3",
"@swc/core-win32-arm64-msvc": "1.15.3",
"@swc/core-win32-ia32-msvc": "1.15.3",
"@swc/core-win32-x64-msvc": "1.15.3"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz",
"integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz",
"integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz",
"integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz",
"integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz",
"integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz",
"integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz",
"integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz",
"integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz",
"integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz",
"integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true
},
"node_modules/@swc/types": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"dev": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -842,6 +1067,44 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
"integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
"dev": true,
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.27",
"@swc/core": "^1.12.11"
},
"peerDependencies": {
"vite": "^4 || ^5 || ^6 || ^7"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -1148,6 +1411,12 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -1499,6 +1768,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -1519,6 +1793,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -1912,6 +2197,29 @@
],
"license": "MIT"
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -2033,6 +2341,14 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2300,6 +2616,19 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+7 -1
View File
@@ -9,12 +9,18 @@
"preview": "vite preview"
},
"dependencies": {
"gsap": "^3.12.5"
"gsap": "^3.12.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.0",
"vite": "^5.2.11"
}
}
+975
View File
@@ -0,0 +1,975 @@
import React, { useEffect, useRef, useState } from 'react'
import gsap from 'gsap'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = '/api'
const FACR_API_URL = 'https://facr.tdvorak.dev'
type Club = {
id: string
name: string
type?: string
website?: string
logo_url?: string
}
type ExistingLogo = {
id: string
}
type ClubResult = Club & {
existingLogo?: boolean
logoUrl?: string
}
type SelectedFile = {
file: File
ext: string
name: string
description: string
}
type Notification = {
message: string
type: 'success' | 'error' | 'info'
} | null
const ClubLogoImage: React.FC<{ src?: string; alt: string }> = ({ src, alt }) => {
const [errored, setErrored] = useState(false)
if (!src || errored) {
return (
<svg
className="w-8 h-8 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
)
}
return (
<img
src={src}
alt={alt}
className="max-w-full max-h-full object-contain"
onError={() => setErrored(true)}
/>
)
}
const AdminApp: React.FC = () => {
const [clubSearchQuery, setClubSearchQuery] = useState('')
const [clubs, setClubs] = useState<ClubResult[]>([])
const [clubSearchLoading, setClubSearchLoading] = useState(false)
const [searchError, setSearchError] = useState<string | null>(null)
const [clubUuid, setClubUuid] = useState('')
const [clubName, setClubName] = useState('')
const [clubType, setClubType] = useState<'football' | 'futsal'>('football')
const [clubWebsite, setClubWebsite] = useState('')
const [uploadVisible, setUploadVisible] = useState(false)
const [isEditMode, setIsEditMode] = useState(false)
const [websiteSearchLoading, setWebsiteSearchLoading] = useState(false)
const [websiteSearchUrl, setWebsiteSearchUrl] = useState<string | null>(null)
const [logoUrlInput, setLogoUrlInput] = useState('')
const [loadFromUrlLoading, setLoadFromUrlLoading] = useState(false)
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([])
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState<{ uploaded: number; total: number } | null>(
null,
)
const [dragOver, setDragOver] = useState(false)
const [notification, setNotification] = useState<Notification>(null)
const searchTimeoutRef = useRef<number | null>(null)
const searchResultsRef = useRef<HTMLDivElement | null>(null)
const uploadSectionRef = useRef<HTMLElement | null>(null)
const filesPreviewAreaRef = useRef<HTMLDivElement | null>(null)
const fileInputRef = useRef<HTMLInputElement | null>(null)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
setNotification({ message, type })
window.setTimeout(() => setNotification(null), 3000)
}
const searchClubs = async (query: string) => {
setClubSearchLoading(true)
setSearchError(null)
try {
const response = await fetch(`${API_BASE_URL}/clubs/search?q=${encodeURIComponent(query)}`)
if (!response.ok) throw new Error('API nedostupné')
const contentType = response.headers.get('content-type') || ''
if (!contentType.includes('application/json')) throw new Error('API vrátilo neplatnou odpověď')
const clubsData = (await response.json()) as Club[]
let existingLogos: ExistingLogo[] = []
try {
const logosResponse = await fetch(`${API_BASE_URL}/logos`)
if (logosResponse.ok) {
const logosContentType = logosResponse.headers.get('content-type') || ''
if (logosContentType.includes('application/json')) {
const logosData = (await logosResponse.json()) as ExistingLogo[]
existingLogos = logosData || []
}
}
} catch {
// optional
}
const enriched: ClubResult[] = clubsData.map((club) => {
const existingLogo = existingLogos.find((l) => l.id === club.id)
let logoUrl = ''
if (existingLogo) logoUrl = `${API_BASE_URL}/logos/${club.id}`
else if (club.logo_url) logoUrl = club.logo_url
return { ...club, existingLogo: Boolean(existingLogo), logoUrl: logoUrl || undefined }
})
setClubs(enriched)
} catch (error: any) {
if (!String(error?.message || '').includes('<!DOCTYPE')) {
console.warn('Search failed:', error?.message || error)
}
setClubs([])
setSearchError('Hledání dočasně nedostupné')
} finally {
setClubSearchLoading(false)
}
}
const handleSelectClub = (club: ClubResult) => {
setClubUuid(club.id)
setClubName(club.name)
setClubType((club.type as 'football' | 'futsal') || 'football')
setClubWebsite(club.website || '')
setUploadVisible(true)
window.setTimeout(() => {
if (uploadSectionRef.current) {
uploadSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' })
gsap.from(uploadSectionRef.current, {
duration: 0.5,
opacity: 0,
y: 20,
ease: 'power2.out',
})
}
}, 0)
showNotification(`Vybráno: ${club.name}`, 'success')
}
const handleSearchWebsite = () => {
const name = clubName.trim()
if (!name) {
showNotification('Nejprve zadejte název klubu', 'error')
return
}
setWebsiteSearchLoading(true)
try {
const searchQuery = encodeURIComponent(`${name} český fotbal oficiální web`)
const searchUrl = `https://www.google.com/search?q=${searchQuery}`
setWebsiteSearchUrl(searchUrl)
} catch (error) {
console.error('Website search error:', error)
} finally {
setWebsiteSearchLoading(false)
}
}
const handleFilesSelect = (files: File[]) => {
const validFiles: SelectedFile[] = []
for (const file of files) {
const ext = file.name.split('.').pop()?.toLowerCase() || ''
if (ext === 'svg' || ext === 'png' || ext === 'pdf') {
validFiles.push({ file, ext, name: '', description: '' })
}
}
if (validFiles.length === 0) {
showNotification('Vyberte prosím SVG, PNG nebo PDF soubory', 'error')
return
}
setSelectedFiles(validFiles)
}
const handleFileMetadataChange = (
index: number,
field: 'name' | 'description',
value: string,
) => {
setSelectedFiles((prev) => {
const next = [...prev]
if (!next[index]) return prev
next[index] = { ...next[index], [field]: value }
return next
})
}
const handleRemoveFile = (index: number) => {
setSelectedFiles((prev) => {
const next = [...prev]
next.splice(index, 1)
if (next.length === 0 && fileInputRef.current) fileInputRef.current.value = ''
return next
})
}
const handleLoadFromUrl = async () => {
const url = logoUrlInput.trim()
if (!url) {
showNotification('Zadejte prosím URL obrázku', 'error')
return
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
showNotification('URL musí začínat http:// nebo https://', 'error')
return
}
setLoadFromUrlLoading(true)
try {
const response = await fetch(url)
if (!response.ok) throw new Error('Nelze načíst obrázek')
const blob = await response.blob()
let ext = 'png'
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('svg')) ext = 'svg'
else if (contentType.includes('pdf')) ext = 'pdf'
else if (contentType.includes('png')) ext = 'png'
else {
const urlExt = url.split('.').pop()?.toLowerCase().split('?')[0]
if (urlExt && ['svg', 'png', 'pdf'].includes(urlExt)) ext = urlExt
}
const filename = `logo-${Date.now()}.${ext}`
const file = new File([blob], filename, { type: blob.type })
handleFilesSelect([file])
showNotification('Obrázek úspěšně načten z URL', 'success')
} catch (error: any) {
console.error('Load from URL error:', error)
showNotification(`Chyba načítání: ${error?.message || 'Chyba'}`, 'error')
} finally {
setLoadFromUrlLoading(false)
}
}
const resetFormAfterUpload = () => {
setClubUuid('')
setClubName('')
setClubType('football')
setClubWebsite('')
setSelectedFiles([])
setUploadVisible(false)
setClubSearchQuery('')
setClubs([])
setWebsiteSearchUrl(null)
setLogoUrlInput('')
if (fileInputRef.current) fileInputRef.current.value = ''
}
const uploadLogos = async (
uuid: string,
clubNameValue: string,
clubTypeValue: string,
clubWebsiteValue: string,
filesData: SelectedFile[],
) => {
setUploading(true)
setUploadProgress({ uploaded: 0, total: filesData.length })
try {
let uploadedCount = 0
for (let i = 0; i < filesData.length; i++) {
const fileData = filesData[i]
const formData = new FormData()
formData.append('file', fileData.file)
formData.append('club_name', clubNameValue)
if (clubTypeValue) formData.append('club_type', clubTypeValue)
if (clubWebsiteValue) formData.append('club_website', clubWebsiteValue)
if (i > 0) {
formData.append('variant', 'true')
if (fileData.name) formData.append('variant_name', fileData.name)
if (fileData.description) formData.append('variant_description', fileData.description)
} else {
if (fileData.name) formData.append('variant_name', fileData.name || 'Hlavní')
if (fileData.description) formData.append('variant_description', fileData.description)
}
const response = await fetch(`${API_BASE_URL}/logos/${uuid}`, {
method: 'POST',
body: formData,
})
if (!response.ok) {
let message = 'Upload failed'
try {
const errorData = await response.json()
if (errorData && errorData.error) message = errorData.error
} catch {
// ignore
}
throw new Error(message)
}
uploadedCount++
setUploadProgress({ uploaded: uploadedCount, total: filesData.length })
}
showNotification(
`${uploadedCount} ${uploadedCount === 1 ? 'logo' : 'loga'} úspěšně nahráno pro ${
clubNameValue || uuid
}! ✓`,
'success',
)
window.setTimeout(() => {
resetFormAfterUpload()
}, 2000)
} catch (error: any) {
console.error('Upload error:', error)
showNotification(`Nahrání selhalo: ${error?.message || 'Chyba'}`, 'error')
} finally {
setUploading(false)
setUploadProgress(null)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const uuid = clubUuid.trim()
const name = clubName.trim()
const type = clubType
const website = clubWebsite.trim()
if (!uuid) {
showNotification('Nejprve vyberte klub', 'error')
return
}
if (selectedFiles.length === 0) {
showNotification('Vyberte prosím soubor loga', 'error')
return
}
if (!uuidRegex.test(uuid)) {
showNotification('Neplatný formát UUID', 'error')
return
}
await uploadLogos(uuid, name, type, website, selectedFiles)
}
useEffect(() => {
console.log('🇨🇿 České Kluby Loga API - Administrace')
console.log('Backend API:', API_BASE_URL)
console.log('FAČR API:', FACR_API_URL)
}, [])
useEffect(() => {
try {
const params = new URLSearchParams(window.location.search)
const editId = params.get('id')
if (editId) {
setIsEditMode(true)
setClubUuid(editId)
setUploadVisible(true)
showNotification('Režim úprav pro existující logo', 'info')
;(async () => {
try {
const resp = await fetch(`${API_BASE_URL}/logos/${editId}/json`)
if (resp.ok) {
const contentType = resp.headers.get('content-type') || ''
if (contentType.includes('application/json')) {
const data = await resp.json()
if (data.club_name) setClubName(data.club_name)
if (data.club_type) setClubType(data.club_type)
if (data.club_website) setClubWebsite(data.club_website)
}
}
} catch {
// non-fatal
}
})()
}
} catch {
// ignore
}
}, [])
useEffect(() => {
const timer = window.setTimeout(() => {
showNotification('Administrace: Vyhledejte kluby a nahrajte loga', 'info')
}, 1000)
return () => window.clearTimeout(timer)
}, [])
useEffect(() => {
if (searchTimeoutRef.current) window.clearTimeout(searchTimeoutRef.current)
const query = clubSearchQuery.trim()
if (query.length < 2) {
setClubs([])
setSearchError(null)
return
}
searchTimeoutRef.current = window.setTimeout(() => {
searchClubs(query)
}, 300)
}, [clubSearchQuery])
useEffect(() => {
if (!searchResultsRef.current || clubs.length === 0) return
const items = searchResultsRef.current.querySelectorAll('.club-result')
if (!items.length) return
gsap.from(items, {
duration: 0.4,
opacity: 0,
y: 20,
stagger: 0.08,
ease: 'power2.out',
})
}, [clubs])
useEffect(() => {
if (!filesPreviewAreaRef.current || selectedFiles.length === 0) return
const items = filesPreviewAreaRef.current.querySelectorAll('[data-file-index]')
if (!items.length) return
gsap.from(items, {
duration: 0.4,
opacity: 0,
y: 10,
stagger: 0.05,
ease: 'power2.out',
})
}, [selectedFiles])
return (
<>
{/* Navigation */}
<TopNav active="admin" />
{/* Admin Header */}
<header className="border-b border-dark-border bg-dark-card">
<div className="container mx-auto px-6 py-8">
<h1 className="text-3xl font-bold gradient-text mb-2">Administrace</h1>
<p className="text-gray-400">Vyhledejte kluby a nahrajte jejich loga</p>
</div>
</header>
<main className="container mx-auto px-6 py-12">
{/* Club Search Section */}
<section className="mb-12">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-2">Krok 1: Vyhledat klub</h2>
<p className="text-sm text-gray-400 mb-4">
Začněte psát název klubu nebo města, poté vyberte správný klub ze seznamu.
</p>
<div className="relative mb-6">
<span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-gray-500">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-4.35-4.35M11 5a6 6 0 100 12 6 6 0 000-12z"
/>
</svg>
</span>
<input
type="text"
id="clubSearch"
placeholder="Hledat české kluby (např. Sparta, Slavia)..."
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 pl-9 py-3 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubSearchQuery}
onChange={(e) => setClubSearchQuery(e.target.value)}
/>
</div>
{/* Search Results */}
<div id="searchResults" className="space-y-3" ref={searchResultsRef}>
{clubSearchLoading && (
<div className="text-center py-4">
<div className="spinner mx-auto" />
</div>
)}
{!clubSearchLoading && searchError && (
<div className="text-center py-4 text-yellow-400">
<p className="mb-2">Hledání dočasně nedostupné</p>
<p className="text-xs text-gray-400">Zkontrolujte, zda běží backend server</p>
</div>
)}
{!clubSearchLoading &&
!searchError &&
clubSearchQuery.trim().length >= 2 &&
clubs.length === 0 && (
<div className="text-center py-8 text-gray-400">
<p>Žádné kluby nenalezeny</p>
</div>
)}
{!clubSearchLoading &&
!searchError &&
clubs.map((club) => (
<div
key={club.id}
className="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer"
>
<div className="flex items-center gap-4">
<div className="flex-shrink-0 w-16 h-16 flex items-center justify-center bg-dark-border/30 rounded-lg p-2">
<ClubLogoImage src={club.logoUrl} alt={club.name} />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg truncate">{club.name}</h3>
<p className="text-sm text-gray-400">{club.type || 'football'}</p>
<p className="text-xs text-gray-500 font-mono mt-1 truncate">{club.id}</p>
{club.website && (
<p className="text-xs text-blue-400 mt-1 truncate">{club.website}</p>
)}
{club.existingLogo && (
<p className="text-xs text-green-400 mt-1">Logo již nahráno</p>
)}
</div>
<div className="flex flex-col gap-2 flex-shrink-0">
{club.existingLogo && (
<a
href={`/logo.html?id=${club.id}`}
className="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-smooth text-sm text-center"
onClick={(e) => e.stopPropagation()}
>
Detail
</a>
)}
<button
type="button"
className="select-club px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm"
onClick={(e) => {
e.stopPropagation()
handleSelectClub(club)
}}
>
Vybrat
</button>
</div>
</div>
</div>
))}
</div>
</div>
</section>
{/* Upload Section */}
<section
id="uploadSection"
ref={uploadSectionRef}
className={uploadVisible ? '' : 'hidden'}
>
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-2">Krok 2: Nahrát logo</h2>
<p className="text-sm text-gray-400 mb-4">
Zkontrolujte údaje o klubu a nahrajte hlavní logo i případné varianty.
</p>
{clubUuid && (
<div className="mb-4 rounded-lg border border-dark-border bg-dark-bg/60 px-4 py-3 text-sm md:flex md:items-center md:justify-between md:gap-4">
<div className="min-w-0">
{isEditMode && (
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-accent-blue">
Režim úprav existujícího loga
</p>
)}
<p className="font-semibold truncate">
{clubName || 'Vybraný klub'}
</p>
<p className="text-xs text-gray-400 truncate">
{clubType === 'futsal' ? 'Futsal' : 'Fotbal'}
{clubWebsite ? `${clubWebsite}` : ''}
</p>
</div>
<div className="mt-3 flex flex-wrap gap-2 md:mt-0 md:ml-4">
<span className="inline-flex items-center rounded-full border border-dark-border px-3 py-1 text-xs text-gray-400 font-mono max-w-full truncate">
{clubUuid}
</span>
<a
href={`/logo.html?id=${clubUuid}`}
className="inline-flex items-center rounded-full border border-dark-border px-3 py-1 text-xs text-accent-blue hover:border-accent-blue transition-smooth"
>
Detail loga
</a>
</div>
</div>
)}
<form id="uploadForm" className="space-y-6" onSubmit={handleSubmit}>
{/* Club UUID (Read-only) */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
UUID Klubu <span className="text-red-500">*</span>
</label>
<input
type="text"
id="clubUuid"
readOnly
className="w-full bg-dark-bg/50 border border-dark-border rounded-lg px-4 py-3 text-gray-400 cursor-not-allowed"
value={clubUuid}
/>
</div>
{/* Club Name (Optional) */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Název Klubu{' '}
<span className="text-gray-500 text-xs">(volitelné)</span>
</label>
<input
type="text"
id="clubName"
placeholder="AC Sparta Praha"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubName}
onChange={(e) => setClubName(e.target.value)}
/>
<p className="text-xs text-gray-500 mt-1">
Volitelné: Pokud název neuvedete, doplníme jej automaticky dle
FAČR (podle UUID)
</p>
</div>
{/* Club Type */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Typ Klubu
</label>
<select
id="clubType"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubType}
onChange={(e) => setClubType(e.target.value as 'football' | 'futsal')}
>
<option value="football">Fotbal</option>
<option value="futsal">Futsal</option>
</select>
</div>
{/* Club Website with Search */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Web Klubu
<button
type="button"
id="searchWebsite"
className="ml-2 text-accent-blue hover:text-blue-400 text-xs disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleSearchWebsite}
disabled={websiteSearchLoading}
>
{websiteSearchLoading ? (
<span className="inline-flex items-center">
<div className="spinner inline-block w-4 h-4" />
</span>
) : (
'Hledat online'
)}
</button>
</label>
<input
type="url"
id="clubWebsite"
placeholder="https://www.sparta.cz"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={clubWebsite}
onChange={(e) => setClubWebsite(e.target.value)}
/>
<div
id="websiteSearchResults"
className={`mt-2 ${websiteSearchUrl ? '' : 'hidden'}`}
>
{websiteSearchUrl && (
<div className="bg-dark-bg rounded-lg p-3 border border-dark-border">
<p className="text-sm text-gray-400 mb-2">Vyhledat web klubu:</p>
<a
href={websiteSearchUrl}
target="_blank"
rel="noreferrer"
className="text-accent-blue hover:text-blue-400 text-sm"
>
Hledat "{clubName || 'klub'}" na Google
</a>
<p className="text-xs text-gray-500 mt-2">
Zkopírujte URL oficiálního webu a vložte jej výše
</p>
</div>
)}
</div>
</div>
{/* File Upload Area */}
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Soubor Loga <span className="text-red-500">*</span>
</label>
{/* URL Upload */}
<div className="mb-3">
<input
type="url"
id="logoUrl"
placeholder="Nebo vložte URL obrázku (https://...)"
className="w-full bg-dark-bg border border-dark-border rounded-lg px-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
value={logoUrlInput}
onChange={(e) => setLogoUrlInput(e.target.value)}
/>
<button
type="button"
id="loadFromUrl"
className="mt-2 px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-smooth text-sm disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleLoadFromUrl}
disabled={loadFromUrlLoading}
>
{loadFromUrlLoading ? (
<span className="inline-flex items-center">
<div className="spinner inline-block w-4 h-4" />
</span>
) : (
'Načíst z URL'
)}
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-dark-border" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-dark-card text-gray-400">nebo</span>
</div>
</div>
<div
id="uploadArea"
className={`upload-area rounded-lg p-12 text-center cursor-pointer border-2 border-dashed border-dark-border hover:border-accent-blue transition-smooth mt-3 ${
dragOver ? 'dragover border-accent-blue' : ''
}`}
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => {
e.preventDefault()
setDragOver(true)
}}
onDragLeave={(e) => {
e.preventDefault()
setDragOver(false)
}}
onDrop={(e) => {
e.preventDefault()
setDragOver(false)
const files = Array.from(e.dataTransfer.files || [])
if (files.length > 0) {
handleFilesSelect(files)
}
}}
>
<svg
style={{ width: 75, paddingTop: 20 }}
className="mx-auto h-12 w-12 text-gray-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p className="text-lg mb-2">
Přetáhněte logo sem nebo{' '}
<span className="text-accent-blue font-semibold">
procházet
</span>
</p>
<p className="text-sm text-gray-500">
SVG, PNG nebo PDF Preferováno průhledné pozadí
</p>
<p className="text-xs text-gray-600 mt-2">
SVG a PDF soubory budou automaticky převedeny na PNG
</p>
<input
type="file"
id="fileInput"
accept=".svg,.png,.pdf"
className="hidden"
multiple
ref={fileInputRef}
onChange={(e) => {
const files = Array.from(e.target.files || [])
if (files.length > 0) {
handleFilesSelect(files)
}
}}
/>
</div>
<p className="text-xs text-gray-500 mt-2">
Můžete vybrat více souborů najednou pro nahrání variant
</p>
</div>
{/* Files Preview */}
<div
id="filesPreviewArea"
ref={filesPreviewAreaRef}
className={selectedFiles.length > 0 ? '' : 'hidden'}
>
<h3 className="text-lg font-semibold mb-3">Vybrané soubory</h3>
<div id="filesPreviewList" className="space-y-3">
{selectedFiles.map((fileObj, index) => {
const sizeKB = (fileObj.file.size / 1024).toFixed(2)
const isPrimary = index === 0
const icon =
fileObj.ext === 'svg' ? 'SVG' : fileObj.ext === 'pdf' ? 'PDF' : 'PNG'
return (
<div
key={index}
className="bg-dark-bg rounded-lg p-4 border border-dark-border"
data-file-index={index}
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-16 h-16 bg-dark-border/30 rounded flex items-center justify-center">
<span className="text-2xl">{icon}</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h4 className="font-semibold">{fileObj.file.name}</h4>
{isPrimary && (
<span className="px-2 py-0.5 bg-accent-blue rounded text-xs">
Hlavní
</span>
)}
</div>
<p className="text-xs text-gray-400 mb-3">
{fileObj.ext.toUpperCase()} {sizeKB} KB
</p>
<div className="space-y-2">
<input
type="text"
placeholder="Název varianty (volitelné)"
value={fileObj.name}
onChange={(e) =>
handleFileMetadataChange(index, 'name', e.target.value)
}
className="w-full bg-dark-card border border-dark-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
<input
type="text"
placeholder="Popis (volitelné)"
value={fileObj.description}
onChange={(e) =>
handleFileMetadataChange(index, 'description', e.target.value)
}
className="w-full bg-dark-card border border-dark-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
</div>
</div>
<button
type="button"
onClick={() => handleRemoveFile(index)}
className="flex-shrink-0 p-2 text-red-400 hover:text-red-300 transition-smooth"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
)
})}
</div>
</div>
{/* Upload Button */}
<button
type="submit"
id="uploadSubmit"
className="w-full px-6 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth disabled:opacity-50 disabled:cursor-not-allowed text-lg"
disabled={uploading}
>
{uploading && uploadProgress ? (
<span className="inline-flex items-center justify-center gap-2">
<div className="spinner mx-auto" />
<span>
{uploadProgress.uploaded}/{uploadProgress.total}
</span>
</span>
) : (
isEditMode ? 'Aktualizovat logo' : 'Nahrát logo'
)}
</button>
{/* Requirements Notice */}
<div className="bg-red-900/20 border border-red-800 rounded-lg p-4 text-sm">
<p className="font-semibold text-red-400 mb-2">
Požadavky na nahrání:
</p>
<ul className="list-disc list-inside space-y-1 text-red-300/80">
<li>
Název klubu je volitelný (doplníme dle FAČR podle UUID)
</li>
<li>UUID klubu musí být platné</li>
<li>Akceptovány pouze SVG, PNG a PDF soubory</li>
<li>Doporučeno průhledné pozadí</li>
</ul>
</div>
</form>
</div>
</section>
</main>
{/* Footer */}
<SiteFooter caption="České Kluby Loga API" />
{notification && (
<div
className={`fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 text-white font-medium ${
notification.type === 'success'
? 'bg-accent-green'
: notification.type === 'error'
? 'bg-red-500'
: 'bg-accent-blue'
}`}
>
{notification.message}
</div>
)}
</>
)
}
export default AdminApp
+348
View File
@@ -0,0 +1,348 @@
import React, { useEffect, useState } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = '/api'
type Logo = {
id: string
club_name: string
club_city?: string
club_type?: string
has_svg?: boolean
has_png?: boolean
}
gsap.registerPlugin(ScrollTrigger)
function useHomeAnimations() {
useEffect(() => {
gsap.from('.hero-content', {
duration: 1,
opacity: 0,
y: 50,
ease: 'power3.out',
delay: 0.2,
})
gsap.utils.toArray<HTMLElement>('.feature-card').forEach((card, index) => {
gsap.from(card, {
scrollTrigger: {
trigger: card,
start: 'top 80%',
toggleActions: 'play none none reverse',
},
duration: 0.6,
opacity: 0,
y: 30,
delay: index * 0.1,
ease: 'power2.out',
})
})
}, [])
}
function useRecentLogos() {
const [logos, setLogos] = useState<Logo[]>([])
const [filtered, setFiltered] = useState<Logo[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [search, setSearch] = useState('')
useEffect(() => {
const load = async () => {
try {
const resp = await fetch(`${API_BASE_URL}/logos?sort=recent&limit=8`)
if (!resp.ok) throw new Error('Failed to fetch recent logos')
const data: Logo[] = await resp.json()
setLogos(data)
setFiltered(data)
} catch (e) {
console.error(e)
setError('Načtení log selhalo')
} finally {
setLoading(false)
}
}
load()
}, [])
useEffect(() => {
const q = search.trim().toLowerCase()
if (!q) {
setFiltered(logos)
return
}
setFiltered(
logos.filter((logo) => logo.club_name.toLowerCase().includes(q))
)
}, [search, logos])
return {
logos,
filtered,
loading,
error,
search,
setSearch,
}
}
const App: React.FC = () => {
useHomeAnimations()
const { filtered, loading, error, search, setSearch } = useRecentLogos()
return (
<>
<TopNav active="home" />
<header className="relative overflow-hidden border-b border-dark-border">
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/10 to-green-600/10" />
<div className="container mx-auto px-6 py-20 relative z-10">
<div className="text-center hero-content max-w-4xl mx-auto">
<h1 className="text-5xl md:text-7xl font-bold mb-6">
<span className="gradient-text">České Kluby Loga CDN</span>
</h1>
<p className="text-xl text-gray-400 mb-8">
Vysoce kvalitní loga českých fotbalových a futsalových klubů s
průhledným pozadím. Založeno na UUID, API-first, připraveno pro
produkci.
</p>
<div className="flex flex-wrap gap-4 justify-center">
<button
onClick={() => (window.location.href = '/logos.html')}
className="px-8 py-4 bg-accent-blue rounded-lg font-semibold hover:bg-blue-600 transition-smooth text-lg"
>
Procházet loga
</button>
<a
href="/admin.html"
className="px-8 py-4 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth text-lg"
>
Nahrát logo
</a>
</div>
</div>
</div>
</header>
<section className="container mx-auto px-6 py-16" id="logoGallery">
<div className="bg-dark-card border border-dark-border rounded-3xl px-6 py-6 md:px-8 md:py-8 shadow-sm">
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-3xl font-bold mb-1">Dostupná loga klubů</h2>
<p className="text-sm text-gray-500">
Nejnovější nahraná loga z registru, připravená pro vaše aplikace.
</p>
</div>
<div className="w-full md:w-auto flex flex-col items-stretch gap-3 md:flex-row md:items-center md:gap-4">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filtrovat podle názvu klubu..."
className="w-full md:w-64 bg-dark-bg border border-dark-border rounded-lg px-4 py-2.5 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
<button
type="button"
onClick={() => (window.location.href = '/logos.html')}
className="inline-flex items-center justify-center px-4 py-2.5 text-sm rounded-lg border border-dark-border bg-dark-bg hover:border-accent-blue transition-smooth"
>
Zobrazit všechna loga
</button>
</div>
</div>
{loading && (
<div className="text-center py-12">
<div className="spinner mx-auto" />
<p className="mt-4 text-gray-400">Načítání log klubů...</p>
</div>
)}
{!loading && error && (
<div className="text-center py-12 text-red-400">{error}</div>
)}
{!loading && !error && filtered.length === 0 && (
<div className="text-center py-12">
<p className="text-lg text-gray-400 mb-4">
Zatím nebyla nahrána žádná loga
</p>
<a
href="/admin.html"
className="px-6 py-3 bg-accent-green rounded-lg font-semibold hover:bg-green-600 transition-smooth inline-block text-sm"
>
Nahrát první logo
</a>
</div>
)}
{!loading && !error && filtered.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4 md:gap-6">
{filtered.map((logo) => {
const logoUrl = `${API_BASE_URL}/logos/${logo.id}`
return (
<button
key={logo.id}
type="button"
onClick={() =>
(window.location.href = `/logo.html?id=${logo.id}`)
}
className="logo-card bg-dark-card rounded-xl p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer group text-left"
>
<div className="aspect-square bg-dark-bg rounded-lg flex items-center justify-center mb-3 overflow-hidden">
<img
src={logoUrl}
alt={logo.club_name}
className="max-w-full max-h-full object-contain p-2 group-hover:scale-110 transition-transform duration-300"
loading="lazy"
onError={(e) => {
const el = e.currentTarget.parentElement
if (!el) return
el.innerHTML =
"<svg class='w-8 h-8 text-gray-500' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'></path></svg>"
}}
/>
</div>
<h3 className="font-semibold text-sm truncate mb-1">
{logo.club_name}
</h3>
<p className="text-xs text-gray-400 truncate">
{logo.club_city
? `${logo.club_city} b7 ${logo.club_type || 'fotbal'}`
: logo.club_type || 'fotbal'}
</p>
<div className="flex gap-1 mt-2">
{logo.has_svg && (
<span className="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded text-[11px] font-medium">
SVG
</span>
)}
{logo.has_png && (
<span className="px-2 py-0.5 bg-green-500/10 text-green-500 rounded text-[11px] font-medium">
PNG
</span>
)}
</div>
</button>
)
})}
</div>
)}
</div>
</section>
<section className="bg-dark-card border-y border-dark-border py-16">
<div className="container mx-auto px-6">
<h2 className="text-3xl font-bold mb-8 text-center">
Rychlá Referenční API
</h2>
<div className="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">
GET
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos</p>
<p className="text-gray-400 text-sm">
Zobrazit všechna dostupná loga
</p>
</div>
</div>
</div>
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">
GET
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos/:id</p>
<p className="text-gray-400 text-sm">
Získat logo podle UUID (PNG/SVG)
</p>
</div>
</div>
</div>
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-blue/20 text-accent-blue rounded-md text-sm font-mono">
GET
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos/:id/json</p>
<p className="text-gray-400 text-sm">Získat metadata loga</p>
</div>
</div>
</div>
<div className="bg-dark-bg rounded-xl p-6 border border-dark-border">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-accent-green/20 text-accent-green rounded-md text-sm font-mono">
POST
</span>
<div className="flex-1">
<p className="font-mono text-sm mb-2">/logos/:id</p>
<p className="text-gray-400 text-sm">Nahrát nové logo</p>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="container mx-auto px-6 py-16">
<h2 className="text-3xl font-bold mb-12 text-center">Funkce</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Integrace s FAČR</h3>
<p className="text-gray-400">
Přímá integrace s oficiálním českým fotbalovým registrem
</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">SVG &amp; PNG</h3>
<p className="text-gray-400">
Nahrajte SVG, PNG se vygeneruje automaticky
</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Založeno na UUID</h3>
<p className="text-gray-400">
Konzistentní identifikace napříč všemi platformami
</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Připraveno pro CDN</h3>
<p className="text-gray-400">Rychlé, cachovatelné, produkční API</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Bohatá Metadata</h3>
<p className="text-gray-400">Název klubu, město, typ, web v ceně</p>
</div>
<div className="feature-card bg-dark-card rounded-xl p-6 border border-dark-border card-hover">
<h3 className="text-xl font-semibold mb-2">Připraveno pro Docker</h3>
<p className="text-gray-400">
Nasazení jedním příkazem s Docker Compose
</p>
</div>
</div>
</section>
<SiteFooter
caption="České Kluby Loga API | Vytvořeno pro český fotbal"
secondary="Poháněno FAČR Scraper API | Open Source MIT Licence"
/>
</>
)
}
export default App
+537
View File
@@ -0,0 +1,537 @@
import React from 'react'
import { TopNav, SiteFooter } from './layout'
const DocsApp: React.FC = () => {
return (
<>
{/* Navigation */}
<TopNav active="docs" />
{/* Header */}
<header className="border-b border-dark-border bg-dark-card">
<div className="container mx-auto px-6 py-12">
<h1 className="text-4xl font-bold gradient-text mb-3">API dokumentace</h1>
<p className="text-xl text-gray-400">
Kompletní referenční příručka pro České Kluby Loga API
</p>
<div className="mt-6 flex gap-4 items-center flex-wrap">
<div>
<span className="text-sm text-gray-400 mr-2">Frontend (prod):</span>
<code className="bg-dark-bg px-4 py-2 rounded text-accent-blue">
https://loga.sportcreative.eu
</code>
</div>
<div>
<span className="text-sm text-gray-400 mr-2">Backend API (prod):</span>
<code className="bg-dark-bg px-4 py-2 rounded text-accent-green">
https://logoapi.sportcreative.eu
</code>
</div>
</div>
<p className="text-sm text-gray-400 mt-3">
Ve vývojovém prostředí používejte relativní cesty (např.{' '}
<code className="text-accent-blue">/logos</code>), Vite proxy je
přesměruje na backend
</p>
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-6 py-12 space-y-16 max-w-5xl">
{/* Quick Start */}
<section className="mb-16">
<h2 className="text-3xl font-bold mb-6">Rychlý start</h2>
<div className="bg-gradient-to-br from-accent-green/10 to-accent-blue/10 rounded-xl p-6 border-2 border-accent-green/30">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
Nahrání loga klubu - Základní příkaz
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/{club-uuid} \
-F "file=@logo.svg" \
-F "club_name=Název Klubu"`}
</code>
</pre>
<div className="mt-4 space-y-2">
<p className="text-sm text-gray-300">
<strong className="text-accent-green">Povinné:</strong> Club UUID v
URL, soubor loga (SVG/PNG/PDF), název klubu
</p>
<p className="text-sm text-gray-300">
<strong className="text-accent-blue">Volitelné:</strong> club_type,
club_website, club_city
</p>
</div>
</div>
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mt-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
Stažení loga klubu
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`# Produkční API
curl https://logoapi.sportcreative.eu/logos/{uuid}
# Přes frontend (loga.sportcreative.eu)
curl https://loga.sportcreative.eu/api/logos/{uuid}`}
</code>
</pre>
<p className="text-gray-400 mt-3 text-sm">
Vrátí PNG obrázek loga (SVG jako fallback)
</p>
</div>
</section>
{/* Endpoints */}
<section className="mb-16">
<h2 className="text-3xl font-bold mb-6">Endpointy</h2>
{/* List Logos */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">
GET
</span>
<code className="text-lg">/logos</code>
</div>
<p className="text-gray-400 mb-4">Seznam všech nahraných log</p>
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200:
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`[
{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"has_svg": true,
"has_png": true,
"logo_url": "https://logoapi.sportcreative.eu/logos/uuid-here",
"created_at": "2024-01-01T12:00:00Z"
}
]`}</code>
</pre>
</div>
</div>
{/* Get Logo File */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">
GET
</span>
<code className="text-lg">/logos/:id</code>
</div>
<p className="text-gray-400 mb-4">
Získání souboru loga (PNG preferováno, SVG jako fallback)
</p>
<h4 className="text-sm font-semibold mb-2">Query Parameters (volitelné):</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<code className="text-sm">format</code>{' '}
<span className="text-gray-500">string</span> - "png" nebo "svg"
</div>
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200:
</h4>
<p className="text-sm text-gray-400">
Binární data obrázku (image/png nebo image/svg+xml)
</p>
</div>
</div>
{/* Get Logo Metadata */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-blue-600/20 text-blue-400 rounded font-mono text-sm">
GET
</span>
<code className="text-lg">/logos/:id/json</code>
</div>
<p className="text-gray-400 mb-4">
Získání metadat loga ve formátu JSON
</p>
<div className="bg-dark-bg rounded-lg p-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200:
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`{
"id": "uuid-here",
"club_name": "AC Sparta Praha",
"club_type": "football",
"club_website": "https://sparta.cz",
"has_svg": true,
"has_png": true,
"primary_format": "png",
"logo_url": "https://logoapi.sportcreative.eu/logos/uuid-here",
"logo_url_svg": "https://logoapi.sportcreative.eu/logos/uuid-here?format=svg",
"logo_url_png": "https://logoapi.sportcreative.eu/logos/uuid-here?format=png",
"file_size_svg": 12345,
"file_size_png": 54321,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}`}</code>
</pre>
</div>
</div>
{/* Upload Logo */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6 border-2 border-accent-green/40">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 bg-accent-green/20 text-accent-green rounded font-mono text-sm">
POST
</span>
<code className="text-lg">/logos/:id</code>
</div>
<p className="text-gray-400 mb-4">
Nahrání nového loga klubu s kompletními daty (ID klubu, název, logo
soubory)
</p>
<h4 className="text-sm font-semibold mb-2">URL Parameters:</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<code className="text-sm">:id</code>{' '}
<span className="text-red-400">*</span>{' '}
<span className="text-gray-500">UUID</span> - Jedinečné ID klubu
(např.{' '}
<code className="text-xs">
550e8400-e29b-41d4-a716-446655440000
</code>
)
</div>
<h4 className="text-sm font-semibold mb-2">Content-Type:</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<code className="text-sm">multipart/form-data</code>
</div>
<h4 className="text-sm font-semibold mb-2">Form Data (Povinné pole):</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div className="border-l-2 border-red-400 pl-3">
<code className="text-sm font-semibold text-red-400">file</code>{' '}
<span className="text-red-400">*</span>{' '}
<span className="text-gray-500">file (SVG nebo PNG)</span>
<p className="text-xs text-gray-500 mt-1">
Soubor loga. Podporované formáty: SVG (doporučeno), PNG, PDF
</p>
</div>
<div className="border-l-2 border-red-400 pl-3">
<code className="text-sm font-semibold text-red-400">
club_name
</code>{' '}
<span className="text-red-400">*</span>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
Název klubu (např. "AC Sparta Praha")
</p>
</div>
</div>
<h4 className="text-sm font-semibold mb-2">Form Data (Volitelné):</h4>
<div className="bg-dark-bg rounded-lg p-4 mb-4 space-y-3">
<div className="border-l-2 border-blue-400 pl-3">
<code className="text-sm">club_type</code>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
Typ klubu: <code>"football"</code> (výchozí) nebo{' '}
<code>"futsal"</code>
</p>
</div>
<div className="border-l-2 border-blue-400 pl-3">
<code className="text-sm">club_website</code>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
URL webové stránky klubu (např. "https://sparta.cz")
</p>
</div>
<div className="border-l-2 border-blue-400 pl-3">
<code className="text-sm">club_city</code>{' '}
<span className="text-gray-500">string</span>
<p className="text-xs text-gray-500 mt-1">
Město klubu (např. "Praha")
</p>
</div>
</div>
<div className="bg-dark-bg rounded-lg p-4 mb-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">
Response 200 (Úspěch):
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`{
"success": true,
"id": "550e8400-e29b-41d4-a716-446655440000",
"club_name": "AC Sparta Praha",
"has_svg": true,
"has_png": true,
"size_svg": 12543,
"size_png": 45210,
"message": "logo uploaded successfully"
}`}</code>
</pre>
</div>
<div className="bg-red-900/20 rounded-lg p-4 border border-red-600/30">
<h4 className="text-sm font-semibold text-red-400 mb-2">
Response 400 (Chyba):
</h4>
<pre className="text-sm overflow-x-auto">
<code>{`{
"error": "club_name is required"
}`}</code>
</pre>
<p className="text-xs text-gray-400 mt-2">
Možné chyby: <code>"no file provided"</code>,{' '}
<code>"invalid UUID format"</code>,{' '}
<code>"only .svg, .png and .pdf files are allowed"</code>
</p>
</div>
</div>
</section>
{/* Examples */}
<section className="mb-16">
<h2 className="text-3xl font-bold mb-6">
Příklady použití nahrání loga
</h2>
{/* cURL Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
cURL (terminal)
</h3>
<div className="space-y-4">
<div>
<h4 className="text-sm font-semibold mb-2 text-accent-green">
Minimální nahrání (pouze povinná pole):
</h4>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha"`}
</code>
</pre>
</div>
<div>
<h4 className="text-sm font-semibold mb-2 text-accent-blue">
Kompletní nahrání (všechna data):
</h4>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.svg" \
-F "club_name=AC Sparta Praha" \
-F "club_type=football" \
-F "club_website=https://sparta.cz" \
-F "club_city=Praha"`}
</code>
</pre>
</div>
<div>
<h4 className="text-sm font-semibold mb-2 text-gray-400">
Nahrání PNG místo SVG:
</h4>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`curl -X POST https://logoapi.sportcreative.eu/logos/550e8400-e29b-41d4-a716-446655440000 \
-F "file=@sparta_logo.png" \
-F "club_name=AC Sparta Praha"`}
</code>
</pre>
</div>
</div>
</div>
{/* JavaScript Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
JavaScript (Fetch API)
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`// Funkce pro nahrání loga s kompletními daty
async function uploadClubLogo(clubId, file, clubData) {
const formData = new FormData();
// Povinná pole
formData.append('file', file);
formData.append('club_name', clubData.name);
// Volitelná pole
if (clubData.type) formData.append('club_type', clubData.type);
if (clubData.website) formData.append('club_website', clubData.website);
if (clubData.city) formData.append('club_city', clubData.city);
const response = await fetch(
'https://logoapi.sportcreative.eu/logos/' + clubId,
{
method: 'POST',
body: formData,
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return await response.json();
}
// Použití s file input
const fileInput = document.getElementById('logoFile');
const clubId = '550e8400-e29b-41d4-a716-446655440000';
const result = await uploadClubLogo(clubId, fileInput.files[0], {
name: 'AC Sparta Praha',
type: 'football',
website: 'https://sparta.cz',
city: 'Praha',
});
console.log('Upload successful:', result);`}
</code>
</pre>
</div>
{/* Python Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
Python (requests)
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`import requests
def upload_club_logo(club_id, file_path, club_name, **optional_data):
"""
Nahraje logo klubu s kompletními daty
Args:
club_id: UUID klubu
file_path: Cesta k souboru loga
club_name: Název klubu (povinný)
**optional_data: club_type, club_website, club_city
"""
with open(file_path, 'rb') as f:
files = {'file': f}
data = {'club_name': club_name}
data.update(optional_data)
response = requests.post(
f"https://logoapi.sportcreative.eu/logos/{club_id}",
files=files,
data=data,
)
response.raise_for_status()
return response.json()
# Použití
result = upload_club_logo(
club_id='550e8400-e29b-41d4-a716-446655440000',
file_path='sparta_logo.svg',
club_name='AC Sparta Praha',
club_type='football',
club_website='https://sparta.cz',
club_city='Praha',
)
print(f"Upload úspěšný: {result['message']}")
print(f"Has SVG: {result['has_svg']}, Has PNG: {result['has_png']}")`}
</code>
</pre>
</div>
{/* PowerShell Example */}
<div className="bg-dark-card rounded-xl p-6 border border-dark-border mb-6">
<h3 className="text-xl font-semibold mb-4 flex items-center gap-2">
PowerShell
</h3>
<pre className="bg-dark-bg rounded-lg p-4 overflow-x-auto">
<code className="text-sm">
{`# Nahrání loga s kompletními daty
$clubId = "550e8400-e29b-41d4-a716-446655440000"
$logoFile = "C:\\logos\\sparta_logo.svg"
$form = @{
file = Get-Item -Path $logoFile
club_name = "AC Sparta Praha"
club_type = "football"
club_website = "https://sparta.cz"
club_city = "Praha"
}
$result = Invoke-RestMethod -Uri "https://logoapi.sportcreative.eu/logos/$clubId" -Method Post -Form $form
Write-Host "Upload úspěšný: $($result.message)" -ForegroundColor Green
Write-Host "Club: $($result.club_name)" -ForegroundColor Cyan`}
</code>
</pre>
</div>
</section>
{/* Error Codes */}
<section>
<h2 className="text-3xl font-bold mb-6">Chybové kódy</h2>
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<div className="space-y-4">
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-green-600/20 text-green-400 rounded text-sm font-mono">
200
</span>
<div>
<h4 className="font-semibold">OK</h4>
<p className="text-gray-400 text-sm">Požadavek úspěšně dokončen</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">
400
</span>
<div>
<h4 className="font-semibold">Bad Request</h4>
<p className="text-gray-400 text-sm">
Neplatné parametry nebo chybějící povinná pole
</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">
404
</span>
<div>
<h4 className="font-semibold">Not Found</h4>
<p className="text-gray-400 text-sm">Logo nebo klub nenalezen</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="px-3 py-1 bg-red-600/20 text-red-400 rounded text-sm font-mono">
500
</span>
<div>
<h4 className="font-semibold">Internal Server Error</h4>
<p className="text-gray-400 text-sm">Interní chyba serveru</p>
</div>
</div>
</div>
</div>
</section>
</main>
{/* Footer */}
<SiteFooter caption="České Kluby Loga API" />
</>
)
}
export default DocsApp
+495
View File
@@ -0,0 +1,495 @@
import React, { useEffect, useState } from 'react'
import gsap from 'gsap'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = 'https://logoapi.sportcreative.eu'
type LogoVariant = {
url: string
name?: string
description?: string
format: string
size?: number
}
type LogoDetail = {
id: string
club_name: string
club_city?: string
club_type?: string
club_website?: string
has_svg?: boolean
has_png?: boolean
file_size_svg?: number
file_size_png?: number
created_at?: string
variants?: LogoVariant[]
}
type Notification = {
message: string
type: 'success' | 'error' | 'info'
} | null
function formatFileSize(bytes?: number): string {
if (!bytes) return 'N/A'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
function formatDate(dateString?: string): string {
if (!dateString) return 'N/A'
const date = new Date(dateString)
return date.toLocaleDateString('cs-CZ', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
const LogoDetailApp: React.FC = () => {
const [logo, setLogo] = useState<LogoDetail | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [notification, setNotification] = useState<Notification>(null)
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const id = params.get('id')
if (!id) {
setError('Logo s tímto UUID neexistuje')
setLoading(false)
return
}
const loadLogo = async () => {
try {
const response = await fetch(`${API_BASE_URL}/logos/${id}/json`)
if (!response.ok) {
throw new Error('Logo not found')
}
const data: LogoDetail = await response.json()
setLogo(data)
} catch (e) {
console.error('Error loading logo:', e)
setError('Logo s tímto UUID neexistuje')
} finally {
setLoading(false)
}
}
loadLogo()
}, [])
useEffect(() => {
if (!logo) return
gsap.from('.logo-detail-section', {
duration: 0.6,
opacity: 0,
y: 20,
stagger: 0.1,
ease: 'power2.out',
})
}, [logo])
const handleCopy = (text: string, label: string) => {
if (!text) return
navigator.clipboard
.writeText(text)
.then(() => {
setNotification({
message: `URL zkopírováno: ${label}`,
type: 'success',
})
})
.catch(() => {
setNotification({
message: 'Chyba při kopírování',
type: 'error',
})
})
setTimeout(() => {
setNotification(null)
}, 3000)
}
const logoId = logo?.id
const previewUrl = logoId ? `${API_BASE_URL}/logos/${logoId}` : ''
const formats = logo
? [
logo.has_png && {
name: 'PNG',
url: `${API_BASE_URL}/logos/${logoId}?format=png`,
size: formatFileSize(logo.file_size_png),
icon: 'PNG',
color: 'bg-blue-600',
},
logo.has_svg && {
name: 'SVG',
url: `${API_BASE_URL}/logos/${logoId}?format=svg`,
size: formatFileSize(logo.file_size_svg),
icon: 'SVG',
color: 'bg-green-600',
},
].filter(Boolean) as {
name: string
url: string
size: string
icon: string
color: string
}[]
: []
const apiUrlDefault = logoId ? `${API_BASE_URL}/logos/${logoId}` : ''
const apiUrlJson = logoId ? `${API_BASE_URL}/logos/${logoId}/json` : ''
return (
<>
{/* Navigation */}
<TopNav active="logos" />
{/* Main Content */}
<main className="container mx-auto px-6 py-12">
{loading && (
<div className="text-center py-12">
<div className="spinner mx-auto mb-4" />
<p className="text-gray-400">Načítání...</p>
</div>
)}
{!loading && error && (
<div className="text-center py-12">
<svg
className="mx-auto h-16 w-16 text-red-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h2 className="text-2xl font-bold mb-2">Logo nenalezeno</h2>
<p className="text-gray-400 mb-4">Logo s tímto UUID neexistuje</p>
<a
href="/"
className="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth inline-block"
>
Zpět na hlavní stránku
</a>
</div>
)}
{!loading && !error && logo && (
<div className="space-y-8 logo-detail-section">
{/* Header */}
<div className="mb-8 space-y-3">
<div className="text-sm text-gray-500">
<a
href="/logos.html"
className="hover:text-accent-blue transition-smooth"
>
Všechna loga
</a>
<span> / </span>
<span className="text-gray-300">{logo.club_name}</span>
</div>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<h1 className="text-4xl font-bold gradient-text mb-2 truncate">
{logo.club_name}
</h1>
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-400">
<span>{logo.club_type || 'fotbal'}</span>
{logo.club_city && (
<>
<span></span>
<span>{logo.club_city}</span>
</>
)}
{logo.club_website && (
<>
<span></span>
<a
href={logo.club_website}
target="_blank"
rel="noreferrer"
className="hover:text-accent-blue"
>
Oficiální web
</a>
</>
)}
{logo.created_at && (
<>
<span></span>
<span>Nahráno {formatDate(logo.created_at)}</span>
</>
)}
</div>
</div>
{logoId && (
<div className="flex flex-col gap-2 md:items-end">
<a
href={`/admin.html?id=${logoId}`}
className="px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm"
>
Upravit logo
</a>
<a
href="/logos.html"
className="text-xs text-gray-400 hover:text-accent-blue transition-smooth"
>
Zpět na seznam log
</a>
</div>
)}
</div>
</div>
{/* Logo Preview */}
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-8 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Náhled loga</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Light Background */}
<div className="logo-preview-surface logo-preview-light rounded-lg p-8 flex items-center justify-center min-h-[300px]">
{previewUrl && (
<div className="logo-preview-inner">
<img
src={previewUrl}
alt={logo.club_name}
className="max-w-full max-h-64 object-contain"
/>
</div>
)}
</div>
{/* Dark Background */}
<div className="logo-preview-surface logo-preview-dark rounded-lg p-8 flex items-center justify-center min-h-[300px]">
{previewUrl && (
<div className="logo-preview-inner">
<img
src={previewUrl}
alt={logo.club_name}
className="max-w-full max-h-64 object-contain"
/>
</div>
)}
</div>
</div>
</div>
</section>
{/* Available Formats */}
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Dostupné formáty</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{formats.map((format) => (
<a
key={format.name}
href={format.url}
download
className="block bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth"
>
<div className="flex items-center justify-between mb-3">
<span className="text-2xl">{format.icon}</span>
<span
className={`${format.color} px-2 py-1 rounded text-xs font-semibold`}
>
{format.name}
</span>
</div>
<h3 className="font-semibold mb-1">{format.name} Format</h3>
<p className="text-sm text-gray-400">{format.size}</p>
<div className="mt-3 flex items-center text-accent-blue text-sm">
<svg
className="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
Stáhnout
</div>
</a>
))}
</div>
</div>
</section>
{/* Variants */}
{logo.variants && logo.variants.length > 0 && (
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Varianty loga</h2>
<div className="space-y-4">
{logo.variants.map((variant) => (
<div
key={variant.url}
className="bg-dark-bg rounded-lg p-4 border border-dark-border"
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-20 h-20 bg-white rounded flex items-center justify-center p-2">
<img
src={variant.url}
alt={variant.name || 'Varianta'}
className="max-w-full max-h-full object-contain"
/>
</div>
<div className="flex-1">
<h3 className="font-semibold mb-1">
{variant.name || 'Varianta'}
</h3>
{variant.description && (
<p className="text-sm text-gray-400 mb-2">
{variant.description}
</p>
)}
<div className="flex items-center gap-3 text-xs text-gray-500">
<span>{variant.format.toUpperCase()}</span>
<span></span>
<span>{formatFileSize(variant.size)}</span>
</div>
</div>
<a
href={variant.url}
download
className="px-3 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm"
>
</a>
</div>
</div>
))}
</div>
</div>
</section>
)}
{/* Metadata */}
<section className="mb-8 logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Informace</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">UUID</h3>
<p className="font-mono text-sm bg-dark-bg rounded px-3 py-2">
{logo.id}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
Typ Klubu
</h3>
<p className="text-sm bg-dark-bg rounded px-3 py-2">
{logo.club_type || 'fotbal'}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
Webová Stránka
</h3>
<p className="text-sm bg-dark-bg rounded px-3 py-2">
{logo.club_website ? (
<a
href={logo.club_website}
target="_blank"
rel="noreferrer"
className="text-accent-blue hover:underline"
>
{logo.club_website}
</a>
) : (
'N/A'
)}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
Datum Nahrání
</h3>
<p className="text-sm bg-dark-bg rounded px-3 py-2">
{formatDate(logo.created_at)}
</p>
</div>
</div>
</div>
</section>
{/* API Usage */}
<section className="logo-detail-section">
<div className="bg-dark-card rounded-xl p-6 border border-dark-border">
<h2 className="text-2xl font-bold mb-6">Použití API</h2>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
GET Logo (PNG preferováno)
</h3>
<div className="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code>{apiUrlDefault}</code>
<button
type="button"
onClick={() => handleCopy(apiUrlDefault, 'GET /logos/:id')}
className="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth"
>
Kopírovat
</button>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-400 mb-2">
GET Logo s Metadaty (JSON)
</h3>
<div className="bg-dark-bg rounded px-4 py-3 font-mono text-sm flex items-center justify-between">
<code>{apiUrlJson}</code>
<button
type="button"
onClick={() => handleCopy(apiUrlJson, 'GET /logos/:id/json')}
className="px-3 py-1 bg-accent-blue rounded text-xs hover:bg-blue-600 transition-smooth"
>
Kopírovat
</button>
</div>
</div>
</div>
</div>
</section>
</div>
)}
</main>
{/* Footer */}
<SiteFooter caption="České Kluby Loga API" />
{notification && (
<div
className={`fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 text-white font-medium ${
notification.type === 'success'
? 'bg-accent-green'
: notification.type === 'error'
? 'bg-red-500'
: 'bg-accent-blue'
}`}
>
{notification.message}
</div>
)}
</>
)
}
export default LogoDetailApp
+363
View File
@@ -0,0 +1,363 @@
import React, { useCallback, useEffect, useState } from 'react'
import { TopNav, SiteFooter } from './layout'
const API_BASE_URL = '/api'
const PAGE_SIZE = 20
type Logo = {
id: string
club_name: string
club_city?: string
club_type?: string
has_svg?: boolean
has_png?: boolean
}
const LogosApp: React.FC = () => {
const [logos, setLogos] = useState<Logo[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [query, setQuery] = useState('')
const [debouncedQuery, setDebouncedQuery] = useState('')
const [typeFilter, setTypeFilter] = useState<'all' | 'football' | 'futsal'>('all')
const [sort, setSort] = useState<'recent' | 'name'>('recent')
const hasActiveFilter =
query.trim().length > 0 || typeFilter !== 'all' || sort !== 'recent'
const resetFilters = () => {
setQuery('')
setTypeFilter('all')
setSort('recent')
setPage(1)
}
useEffect(() => {
const handle = window.setTimeout(() => setDebouncedQuery(query), 300)
return () => window.clearTimeout(handle)
}, [query])
const loadPage = useCallback(
async (reset: boolean) => {
if (loading) return
setLoading(true)
setError(null)
try {
const nextPage = reset ? 1 : page
const url = new URL(`${API_BASE_URL}/logos`, window.location.origin)
url.searchParams.set('sort', sort)
url.searchParams.set('limit', String(PAGE_SIZE))
url.searchParams.set('page', String(nextPage))
if (debouncedQuery) url.searchParams.set('q', debouncedQuery)
if (typeFilter !== 'all') url.searchParams.set('type', typeFilter)
const resp = await fetch(url.toString().replace(window.location.origin, ''))
if (!resp.ok) throw new Error('Failed to fetch logos')
const data: Logo[] = await resp.json()
if (reset) {
setLogos(data)
} else {
setLogos((prev) => [...prev, ...data])
}
if (Array.isArray(data) && data.length === PAGE_SIZE) {
setHasMore(true)
setPage(nextPage + 1)
} else {
setHasMore(false)
}
} catch (e) {
if (reset) {
setError('Načtení log selhalo')
setLogos([])
setHasMore(false)
}
} finally {
setLoading(false)
}
},
[loading, page, debouncedQuery, typeFilter, sort]
)
useEffect(() => {
loadPage(true)
}, [loadPage, debouncedQuery])
const handleDelete = async (id: string) => {
const ok = window.confirm('Smazat toto logo?')
if (!ok) return
try {
const resp = await fetch(`${API_BASE_URL}/logos/${id}`, { method: 'DELETE' })
if (!resp.ok) throw new Error('Delete failed')
setLogos((prev) => prev.filter((l) => l.id !== id))
} catch (_) {
window.alert('Mazání selhalo')
}
}
const isInitialLoading = logos.length === 0 && loading
const showEmpty = !loading && !error && logos.length === 0
return (
<>
<TopNav active="logos" />
<header className="border-b border-dark-border bg-dark-card">
<div className="container mx-auto px-6 py-8">
<h1 className="text-3xl font-bold gradient-text mb-2">Všechna Loga</h1>
<p className="text-gray-400">
Procházejte všechna dostupná loga, vyhledávejte a spravujte
</p>
</div>
</header>
<main className="container mx-auto px-6 py-12">
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div className="w-full md:max-w-lg space-y-2">
<label className="text-xs font-medium text-gray-400 uppercase tracking-wide">
Vyhledat loga
</label>
<div className="relative">
<span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-gray-500">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-4.35-4.35M11 5a6 6 0 100 12 6 6 0 000-12z"
/>
</svg>
</span>
<input
type="text"
value={query}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setQuery(e.target.value)
}
placeholder="Hledat mezi všemi logy..."
className="w-full bg-dark-card border border-dark-border rounded-lg px-4 pl-9 py-3 text-sm text-white focus:outline-none focus:border-accent-blue transition-smooth"
/>
</div>
<p className="text-[11px] text-gray-500">
Filtruje podle názvu klubu, můžete kombinovat s typem klubu a řazením.
</p>
</div>
<div className="w-full md:w-auto space-y-2">
<div className="flex flex-wrap items-center gap-2 justify-between md:justify-end text-[11px] text-gray-500">
<span>{PAGE_SIZE} log na stránku</span>
<span>
řazeno: {sort === 'recent' ? 'nejnovější' : 'podle názvu'}
</span>
</div>
<div className="flex flex-wrap items-center gap-2 justify-start md:justify-end">
<div className="inline-flex flex-wrap gap-1 rounded-full bg-dark-card/60 border border-dark-border px-1 py-1">
<button
type="button"
onClick={() => {
setPage(1)
setTypeFilter('all')
}}
className={`px-3 py-1.5 rounded-full border text-xs ${
typeFilter === 'all'
? 'bg-accent-blue/10 text-accent-blue border-accent-blue'
: 'bg-transparent border-transparent text-gray-400 hover:bg-dark-bg/80'
}`}
>
Vše
</button>
<button
type="button"
onClick={() => {
setPage(1)
setTypeFilter('football')
}}
className={`px-3 py-1.5 rounded-full border text-xs ${
typeFilter === 'football'
? 'bg-accent-blue/10 text-accent-blue border-accent-blue'
: 'bg-transparent border-transparent text-gray-400 hover:bg-dark-bg/80'
}`}
>
Fotbal
</button>
<button
type="button"
onClick={() => {
setPage(1)
setTypeFilter('futsal')
}}
className={`px-3 py-1.5 rounded-full border text-xs ${
typeFilter === 'futsal'
? 'bg-accent-blue/10 text-accent-blue border-accent-blue'
: 'bg-transparent border-transparent text-gray-400 hover:bg-dark-bg/80'
}`}
>
Futsal
</button>
</div>
<select
value={sort}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
setPage(1)
setSort(e.target.value as 'recent' | 'name')
}}
className="bg-dark-card border border-dark-border rounded-lg px-3 py-2 text-xs text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
<option value="recent">Nejnovější</option>
<option value="name">Název (AZ)</option>
</select>
{hasActiveFilter && (
<button
type="button"
onClick={resetFilters}
className="text-[11px] text-gray-400 hover:text-accent-blue underline-offset-2 hover:underline"
>
Vymazat filtry
</button>
)}
</div>
</div>
</div>
{isInitialLoading && (
<div className="text-center py-12">
<div className="spinner mx-auto" />
<p className="mt-4 text-gray-400">Načítání log...</p>
</div>
)}
{!isInitialLoading && error && (
<div className="text-center py-12 text-red-400">{error}</div>
)}
{showEmpty && (
<div className="text-center py-16">
<p className="text-xl text-gray-400 mb-2">Žádná loga nenalezena</p>
{hasActiveFilter ? (
<p className="text-sm text-gray-500 mb-4">
Zkuste upravit vyhledávání nebo typ klubu, případně vymažte filtry.
</p>
) : (
<p className="text-sm text-gray-500 mb-4">
Zatím zde nejsou žádná loga.
</p>
)}
{hasActiveFilter && (
<button
type="button"
onClick={resetFilters}
className="px-4 py-2 bg-dark-card border border-dark-border rounded-lg text-sm text-gray-200 hover:bg-dark-border transition-smooth mr-2"
>
Vymazat filtry
</button>
)}
<a
href="/admin.html"
className="inline-flex items-center justify-center px-4 py-2 bg-accent-green rounded-lg text-sm font-medium hover:bg-green-600 transition-smooth"
>
Nahrát nové logo
</a>
</div>
)}
{!showEmpty && logos.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{logos.map((logo: Logo) => {
const logoUrl = `${API_BASE_URL}/logos/${logo.id}`
return (
<div
key={logo.id}
className="logo-card bg-dark-card rounded-xl p-4 border border-dark-border hover:border-accent-blue transition-smooth group"
>
<button
type="button"
onClick={() =>
(window.location.href = `/logo.html?id=${logo.id}`)
}
className="aspect-square bg-dark-bg rounded-lg flex items-center justify-center mb-3 overflow-hidden cursor-pointer w-full"
>
<img
src={logoUrl}
alt={logo.club_name}
className="max-w-full max-h-full object-contain p-2 group-hover:scale-110 transition-transform duration-300"
loading="lazy"
onError={(e) => {
const el = e.currentTarget.parentElement
if (!el) return
el.innerHTML =
"<svg class='w-8 h-8 text-gray-500' fill='none' stroke='currentColor' viewBox='0 0 24 24'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'></path></svg>"
}}
/>
</button>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
(window.location.href = `/logo.html?id=${logo.id}`)
}
className="flex-1 min-w-0 text-left cursor-pointer"
>
<h3 className="font-semibold text-sm truncate mb-0.5">
{logo.club_name}
</h3>
<p className="text-xs text-gray-400 truncate">
{logo.club_city
? `${logo.club_city} b7 ${logo.club_type || 'fotbal'}`
: logo.club_type || 'fotbal'}
</p>
<div className="mt-1 flex gap-1">
{logo.has_svg && (
<span className="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded text-[11px] font-medium">
SVG
</span>
)}
{logo.has_png && (
<span className="px-2 py-0.5 bg-green-500/10 text-green-500 rounded text-[11px] font-medium">
PNG
</span>
)}
</div>
</button>
<button
type="button"
onClick={() => handleDelete(logo.id)}
className="delete-logo px-3 py-1.5 text-xs bg-red-600 rounded hover:bg-red-500 transition-smooth"
>
Smazat
</button>
</div>
</div>
)
})}
</div>
)}
{hasMore && !isInitialLoading && (
<div className="text-center mt-10">
<button
type="button"
onClick={() => loadPage(false)}
className="px-6 py-3 bg-dark-card border border-dark-border rounded-lg hover:bg-dark-border transition-smooth disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? 'Načítání...' : 'Načíst další'}
</button>
</div>
)}
</main>
<SiteFooter caption="České Kluby Loga API" />
</>
)
}
export default LogosApp
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import AdminApp from './AdminApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<AdminApp />
</React.StrictMode>
)
+11 -10
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
// Configuration
@@ -53,7 +54,7 @@ async function searchClubs(query) {
}
searchResults.innerHTML = `
<div class="text-center py-4 text-yellow-400">
<p class="mb-2">⚠️ Hledání dočasně nedostupné</p>
<p class="mb-2">Hledání dočasně nedostupné</p>
<p class="text-xs text-gray-400">Zkontrolujte, zda běží backend server</p>
</div>
`
@@ -128,11 +129,11 @@ async function displaySearchResults(clubs) {
<h3 class="font-semibold text-lg truncate">${club.name}</h3>
<p class="text-sm text-gray-400">${club.type || 'football'}</p>
<p class="text-xs text-gray-500 font-mono mt-1 truncate">${club.id}</p>
${club.website ? `<p class="text-xs text-blue-400 mt-1 truncate">🌐 ${club.website}</p>` : ''}
${existingLogo ? '<p class="text-xs text-green-400 mt-1">Logo již nahráno</p>' : ''}
${club.website ? `<p class="text-xs text-blue-400 mt-1 truncate">${club.website}</p>` : ''}
${existingLogo ? '<p class="text-xs text-green-400 mt-1">Logo již nahráno</p>' : ''}
</div>
<div class="flex flex-col gap-2 flex-shrink-0">
${existingLogo ? `<a href="/logo.html?id=${club.id}" class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-smooth text-sm text-center" onclick="event.stopPropagation()">👁️ Detail</a>` : ''}
${existingLogo ? `<a href="/logo.html?id=${club.id}" class="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-smooth text-sm text-center" onclick="event.stopPropagation()">Detail</a>` : ''}
<button class="select-club px-4 py-2 bg-accent-blue rounded-lg hover:bg-blue-600 transition-smooth text-sm">
Vybrat
</button>
@@ -210,7 +211,7 @@ searchWebsiteBtn.addEventListener('click', async () => {
<div class="bg-dark-bg rounded-lg p-3 border border-dark-border">
<p class="text-sm text-gray-400 mb-2">Vyhledat web klubu:</p>
<a href="${searchUrl}" target="_blank" class="text-accent-blue hover:text-blue-400 text-sm">
🔍 Hledat "${clubName}" na Google
Hledat "${clubName}" na Google
</a>
<p class="text-xs text-gray-500 mt-2">Zkopírujte URL oficiálního webu a vložte jej výše</p>
</div>
@@ -220,7 +221,7 @@ searchWebsiteBtn.addEventListener('click', async () => {
} catch (error) {
console.error('Website search error:', error)
} finally {
searchWebsiteBtn.innerHTML = '🔍 Hledat Online'
searchWebsiteBtn.innerHTML = 'Hledat Online'
searchWebsiteBtn.disabled = false
}
})
@@ -310,7 +311,7 @@ function displayFilesPreview() {
<div class="bg-dark-bg rounded-lg p-4 border border-dark-border" data-file-index="${index}">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-16 h-16 bg-dark-border/30 rounded flex items-center justify-center">
<span class="text-2xl">${fileObj.ext === 'svg' ? '📐' : fileObj.ext === 'pdf' ? '📄' : '🖼️'}</span>
<span class="text-2xl">${fileObj.ext === 'svg' ? 'SVG' : fileObj.ext === 'pdf' ? 'PDF' : 'PNG'}</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
@@ -444,7 +445,7 @@ async function uploadLogos(uuid, clubName, clubType, clubWebsite, filesData) {
submitBtn.innerHTML = `<div class="spinner mx-auto"></div> ${uploadedCount}/${filesData.length}`
}
showNotification(`${uploadedCount} ${uploadedCount === 1 ? 'logo' : 'loga'} úspěšně nahráno pro ${clubName}!`, 'success')
showNotification(`${uploadedCount} ${uploadedCount === 1 ? 'logo' : 'loga'} úspěšně nahráno pro ${clubName}!`, 'success')
// Reset form after delay
setTimeout(() => {
@@ -499,7 +500,7 @@ function showNotification(message, type = 'info') {
// ==================== Initialize ====================
console.log('🇨🇿 České Kluby Loga API - Administrace')
console.log('České Kluby Loga API - Administrace')
console.log('Backend API:', API_BASE_URL)
console.log('FAČR API:', FACR_API_URL)
@@ -586,7 +587,7 @@ loadFromUrlBtn.addEventListener('click', async () => {
showNotification(`Chyba načítání: ${error.message}`, 'error')
} finally {
loadFromUrlBtn.disabled = false
loadFromUrlBtn.innerHTML = '📥 Načíst z URL'
loadFromUrlBtn.innerHTML = 'Načíst z URL'
}
})
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import DocsApp from './DocsApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<DocsApp />
</React.StrictMode>
)
+3 -2
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
@@ -193,7 +194,7 @@ function showNotification(message, type = 'info') {
// ==================== Initialize ====================
console.log('🇨🇿 Czech Clubs Logos API - Home')
console.log('Czech Clubs Logos API - Home')
console.log('Backend API:', API_BASE_URL)
// Load logos on page load
@@ -201,5 +202,5 @@ loadRecentLogos()
// Show welcome notification
setTimeout(() => {
showNotification('Welcome to Czech Clubs Logos API! 🇨🇿', 'info')
showNotification('Welcome to Czech Clubs Logos API!', 'info')
}, 1000)
+79
View File
@@ -0,0 +1,79 @@
import React from 'react'
type TopNavProps = {
active?: 'home' | 'logos' | 'docs' | 'admin'
}
export const TopNav: React.FC<TopNavProps> = ({ active }) => {
return (
<nav className="border-b border-dark-border bg-white/80 dark:bg-dark-card/80 backdrop-blur-md sticky top-0 z-50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-3">
<div className="flex items-center justify-between gap-4">
<a href="/" className="flex items-center gap-2">
<div className="h-8 w-8 rounded-xl bg-gradient-to-br from-accent-blue to-accent-green flex items-center justify-center text-xs font-bold text-white">
CL
</div>
<span className="text-lg sm:text-xl font-semibold tracking-tight">
České Kluby Loga
</span>
</a>
<div className="flex items-center gap-3">
<div className="hidden md:flex gap-1 rounded-full bg-dark-bg/40 dark:bg-dark-bg/60 px-1 py-1 border border-dark-border/80">
<a
href="/"
className={`nav-link ${active === 'home' ? 'nav-link--active' : ''}`}
>
Domů
</a>
<a
href="/logos.html"
className={`nav-link ${active === 'logos' ? 'nav-link--active' : ''}`}
>
Všechna loga
</a>
<a
href="/api-docs.html"
className={`nav-link ${active === 'docs' ? 'nav-link--active' : ''}`}
>
API Docs
</a>
<a
href="/admin.html"
className={`nav-link ${active === 'admin' ? 'nav-link--active' : ''}`}
>
Admin
</a>
</div>
<button
type="button"
data-theme-toggle
className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-dark-border bg-dark-bg/60 hover:bg-dark-border transition-smooth text-xs"
>
<span className="sr-only">Přepnout téma</span>
<span data-theme-icon></span>
</button>
</div>
</div>
</div>
</nav>
)
}
type SiteFooterProps = {
caption?: string
secondary?: string
}
export const SiteFooter: React.FC<SiteFooterProps> = ({
caption = 'České Kluby Loga API',
secondary,
}) => {
return (
<footer className="border-t border-dark-border mt-20">
<div className="container mx-auto px-6 py-8 text-center text-gray-400 text-sm">
<p>{caption}</p>
{secondary && <p className="text-xs sm:text-sm mt-2">{secondary}</p>}
</div>
</footer>
)
}
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import LogoDetailApp from './LogoDetailApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<LogoDetailApp />
</React.StrictMode>
)
+4 -3
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
// Configuration
@@ -62,7 +63,7 @@ function displayLogoDetails(logo) {
name: 'PNG',
url: `${API_BASE_URL}/logos/${logoId}?format=png`,
size: formatFileSize(logo.file_size_png),
icon: '🖼️',
icon: 'PNG',
color: 'bg-blue-600'
})
}
@@ -72,7 +73,7 @@ function displayLogoDetails(logo) {
name: 'SVG',
url: `${API_BASE_URL}/logos/${logoId}?format=svg`,
size: formatFileSize(logo.file_size_svg),
icon: '📐',
icon: 'SVG',
color: 'bg-green-600'
})
}
@@ -216,5 +217,5 @@ function showNotification(message, type = 'info') {
}, 3000)
}
console.log('🇨🇿 České Kluby Loga API - Detail Loga')
console.log('České Kluby Loga API - Detail loga')
console.log('Logo ID:', logoId)
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import LogosApp from './LogosApp'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<LogosApp />
</React.StrictMode>
)
+1
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
const API_BASE_URL = '/api'
+5 -4
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
@@ -217,7 +218,7 @@ function displaySearchResults(clubs) {
// Visual feedback
const originalText = btn.textContent
btn.textContent = 'Copied!'
btn.textContent = 'Copied!'
setTimeout(() => {
btn.textContent = originalText
}, 2000)
@@ -351,7 +352,7 @@ async function uploadLogo(uuid, file) {
throw new Error('Upload failed')
}
showNotification('Logo uploaded successfully!', 'success')
showNotification('Logo uploaded successfully!', 'success')
// Reset form
setTimeout(() => {
@@ -414,11 +415,11 @@ function showNotification(message, type = 'info') {
// ==================== Initialize ====================
console.log('🇨🇿 Czech Clubs Logos API Frontend')
console.log('Czech Clubs Logos API Frontend')
console.log('Backend API:', API_BASE_URL)
console.log('FAČR API:', FACR_API_URL)
// Show a welcome notification
setTimeout(() => {
showNotification('Welcome to Czech Clubs Logos API! 🇨🇿', 'info')
showNotification('Welcome to Czech Clubs Logos API!', 'info')
}, 1000)
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './style.css'
import './theme.js'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
+153 -1
View File
@@ -86,7 +86,7 @@ body {
/* Loading spinner */
.spinner {
border: 3px solid rgba(255, 255, 255, 0.1);
border: 3px solid rgba(148, 163, 184, 0.25);
border-radius: 50%;
border-top-color: #3b82f6;
width: 40px;
@@ -97,3 +97,155 @@ body {
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Theme-aware tokens (light first, dark via .dark) */
.bg-dark-bg {
background-color: #f3f4f6;
}
.dark .bg-dark-bg {
background-color: #0a0e1a;
}
.bg-dark-card {
background-color: #ffffff;
}
.dark .bg-dark-card {
background-color: #131823;
}
.bg-dark-border {
background-color: #e5e7eb;
}
.dark .bg-dark-border {
background-color: #1f2937;
}
.border-dark-border {
border-color: #e5e7eb;
}
.dark .border-dark-border {
border-color: #1f2937;
}
body {
background-color: #f9fafb;
}
.dark body {
background-color: #020617;
}
.bg-dark-card .text-white,
.bg-dark-bg .text-white {
color: #020617;
}
.dark .bg-dark-card .text-white,
.dark .bg-dark-bg .text-white {
color: #f9fafb;
}
.nav-link {
font-size: 0.875rem;
font-weight: 500;
border-radius: 9999px;
padding-inline: 0.9rem;
padding-block: 0.5rem;
color: #6b7280;
}
.dark .nav-link {
color: #9ca3af;
}
.nav-link:hover {
background-color: rgba(15, 23, 42, 0.04);
color: #111827;
}
.dark .nav-link:hover {
background-color: rgba(148, 163, 184, 0.12);
color: #e5e7eb;
}
.nav-link--active {
background-color: rgba(59, 130, 246, 0.12);
color: #1d4ed8;
}
.dark .nav-link--active {
background-color: rgba(59, 130, 246, 0.18);
color: #bfdbfe;
}
.logo-card {
position: relative;
overflow: hidden;
}
.logo-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: 0.75rem;
border: 1px solid transparent;
pointer-events: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.logo-card:hover::before {
border-color: rgba(59, 130, 246, 0.4);
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.16);
}
.dark .logo-card:hover::before {
border-color: rgba(59, 130, 246, 0.6);
box-shadow: 0 22px 55px rgba(15, 23, 42, 0.9);
}
.logo-card:hover {
transform: translateY(-2px);
}
.logo-preview-surface {
position: relative;
overflow: hidden;
}
.logo-preview-surface::before {
content: '';
position: absolute;
inset: 0;
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
opacity: 0.4;
}
.logo-preview-light {
background-color: #f9fafb;
}
.logo-preview-light::before {
background-image:
linear-gradient(45deg, #f3f4f6 25%, transparent 25%, transparent 75%, #f3f4f6 75%, #f3f4f6),
linear-gradient(45deg, #e5e7eb 25%, transparent 25%, transparent 75%, #e5e7eb 75%, #e5e7eb);
}
.logo-preview-dark {
background-color: #020617;
}
.logo-preview-dark::before {
background-image:
linear-gradient(45deg, #111827 25%, transparent 25%, transparent 75%, #111827 75%, #111827),
linear-gradient(45deg, #020617 25%, transparent 25%, transparent 75%, #020617 75%, #020617);
}
.logo-preview-inner {
position: relative;
z-index: 1;
}
+57
View File
@@ -0,0 +1,57 @@
const STORAGE_KEY = 'clublogos-theme'
function getPreferredTheme() {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored === 'light' || stored === 'dark') return stored
} catch (_) {}
return 'light'
}
function applyTheme(theme) {
const root = document.documentElement
if (theme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}
function initThemeToggle() {
const toggles = document.querySelectorAll('[data-theme-toggle]')
if (!toggles.length) return
const current = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
updateToggleIcons(current)
toggles.forEach((btn) => {
btn.addEventListener('click', () => {
const isDark = document.documentElement.classList.contains('dark')
const next = isDark ? 'light' : 'dark'
applyTheme(next)
updateToggleIcons(next)
try {
localStorage.setItem(STORAGE_KEY, next)
} catch (_) {}
})
})
}
function updateToggleIcons(theme) {
const isDark = theme === 'dark'
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
const iconSpan = btn.querySelector('[data-theme-icon]')
if (iconSpan) {
iconSpan.textContent = isDark ? '☾' : '☀'
}
})
}
const initial = getPreferredTheme()
applyTheme(initial)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initThemeToggle)
} else {
initThemeToggle()
}
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./*.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
+2
View File
@@ -1,7 +1,9 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
root: './',
server: {
port: 3000,