update ui, search, new api endpoint

This commit is contained in:
Tomas Dvorak
2025-12-01 10:05:27 +01:00
parent 025f5beef1
commit 6a9f25ffe9
19 changed files with 2082 additions and 121 deletions
+49 -13
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs" class="dark theme-dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -13,10 +13,15 @@
<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 class="flex items-center gap-3">
<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>
<button id="themeToggle" type="button" class="px-3 py-1.5 text-xs md:text-sm rounded-full border border-dark-border bg-dark-bg/60 hover:bg-dark-border transition-smooth">
☀️ <span class="hidden sm:inline">Světlý režim</span>
</button>
</div>
</div>
</div>
@@ -36,14 +41,30 @@
<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 class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between mb-4">
<div class="w-full md:max-w-lg">
<div class="relative">
<span class="pointer-events-none absolute inset-y-0 left-3 flex items-center text-gray-500">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M10.5 18a7.5 7.5 0 1 1 0-15 7.5 7.5 0 0 1 0 15z" />
</svg>
</span>
<input
type="text"
id="clubSearch"
placeholder="Hledat české kluby (např. Sparta, Slavia)..."
class="w-full bg-dark-bg border border-dark-border rounded-lg pl-10 pr-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
</div>
</div>
<div class="flex flex-col items-start md:items-end gap-1 text-xs">
<div class="inline-flex rounded-full bg-dark-bg border border-dark-border p-1">
<button type="button" data-club-sport-filter="all" class="px-3 py-1.5 rounded-full bg-accent-blue text-white transition-smooth">Vše</button>
<button type="button" data-club-sport-filter="football" class="px-3 py-1.5 rounded-full bg-dark-bg text-gray-300 hover:bg-dark-border transition-smooth">Fotbal</button>
<button type="button" data-club-sport-filter="futsal" class="px-3 py-1.5 rounded-full bg-dark-bg text-gray-300 hover:bg-dark-border transition-smooth">Futsal</button>
</div>
<span class="text-[11px] text-gray-500">Výsledky z FAČR • filtr dle druhu sportu</span>
</div>
</div>
<!-- Search Results -->
@@ -57,6 +78,21 @@
<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>
<div id="selectedClubSummary" class="hidden mb-6">
<div class="bg-dark-bg rounded-lg border border-dark-border p-4 flex items-start gap-4">
<div id="selectedClubLogo" class="flex-shrink-0 w-14 h-14 rounded-lg bg-dark-border/40 flex items-center justify-center text-2xl">
🏟️
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<h3 id="selectedClubName" class="font-semibold truncate"></h3>
<span id="selectedClubType" class="px-2 py-0.5 rounded-full bg-accent-blue/10 text-xs text-accent-blue uppercase tracking-wide"></span>
</div>
<p id="selectedClubCity" class="text-xs text-gray-400 truncate"></p>
<p id="selectedClubWebsite" class="text-xs text-accent-blue mt-1 truncate"></p>
</div>
</div>
</div>
<form id="uploadForm" class="space-y-6">
<!-- Club UUID (Read-only) -->
+46 -5
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs" class="dark theme-dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -13,10 +13,15 @@
<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 class="flex items-center gap-3">
<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>
<button id="themeToggle" type="button" class="px-3 py-1.5 text-xs md:text-sm rounded-full border border-dark-border bg-dark-bg/60 hover:bg-dark-border transition-smooth">
☀️ <span class="hidden sm:inline">Světlý režim</span>
</button>
</div>
</div>
</div>
@@ -79,6 +84,42 @@ curl http://localhost:3000/api/logos/{uuid}</code></pre>
<section class="mb-16">
<h2 class="text-3xl font-bold mb-6">📡 Endpointy</h2>
<!-- Search Clubs with 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">/clubs/search-with-logos</code>
</div>
<p class="text-gray-400 mb-4">Vyhledá kluby v lokální databázi nahraných log (podle názvu nebo ID) a vrátí kompaktní JSON obsahující pouze ID, název, URL loga a příznak lokálního loga.</p>
<h4 class="text-sm font-semibold mb-2">Query Parametry:</h4>
<div class="bg-dark-bg rounded-lg p-4 mb-4 space-y-2">
<div>
<code class="text-sm">q</code> <span class="text-red-400">*</span> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Text pro hledání klubu (např. "Sparta", "Slavia", "Hranice").</p>
</div>
<div>
<code class="text-sm">sport</code> <span class="text-gray-500">string</span>
<p class="text-xs text-gray-500 mt-1">Volitelný filtr podle druhu sportu: <code>"football"</code>, <code>"futsal"</code> nebo <code>"all"</code> (výchozí).
Alias: <code>type</code>.</p>
</div>
</div>
<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": "550e8400-e29b-41d4-a716-446655440000",
"name": "AC Sparta Praha",
"logo_url": "http://localhost:8080/logos/550e8400-e29b-41d4-a716-446655440000?format=png",
"has_local_logo": true
}
]</code></pre>
<p class="text-xs text-gray-500 mt-2">Pozn.: Výsledky se berou pouze z lokální databáze. <code>logo_url</code> vždy míří na tento backend
a <code>has_local_logo</code> indikuje, že pro klub existuje alespoň jedno uložené logo.</p>
</div>
</div>
<!-- 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">
+11 -6
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs" class="dark theme-dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -13,11 +13,16 @@
<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 class="flex items-center gap-3">
<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>
<button id="themeToggle" type="button" class="px-3 py-1.5 text-xs md:text-sm rounded-full border border-dark-border bg-dark-bg/60 hover:bg-dark-border transition-smooth">
☀️ <span class="hidden sm:inline">Světlý režim</span>
</button>
</div>
</div>
</div>
+10 -5
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs" class="dark theme-dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -13,10 +13,15 @@
<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 class="flex items-center gap-3">
<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>
<button id="themeToggle" type="button" class="px-3 py-1.5 text-xs md:text-sm rounded-full border border-dark-border bg-dark-bg/60 hover:bg-dark-border transition-smooth">
☀️ <span class="hidden sm:inline">Světlý režim</span>
</button>
</div>
</div>
</div>
+35 -14
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="cs" class="dark">
<html lang="cs" class="dark theme-dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -11,11 +11,16 @@
<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 class="flex items-center gap-3">
<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>
<button id="themeToggle" type="button" class="px-3 py-1.5 text-xs md:text-sm rounded-full border border-dark-border bg-dark-bg/60 hover:bg-dark-border transition-smooth">
☀️ <span class="hidden sm:inline">Světlý režim</span>
</button>
</div>
</div>
</div>
@@ -29,14 +34,30 @@
</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 class="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="w-full md:max-w-lg">
<div class="relative">
<span class="pointer-events-none absolute inset-y-0 left-3 flex items-center text-gray-500">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M10.5 18a7.5 7.5 0 1 1 0-15 7.5 7.5 0 0 1 0 15z" />
</svg>
</span>
<input
type="text"
id="allLogoSearch"
placeholder="Hledat mezi všemi logy..."
class="w-full bg-dark-card border border-dark-border rounded-lg pl-10 pr-4 py-3 text-white focus:outline-none focus:border-accent-blue transition-smooth"
>
</div>
</div>
<div class="flex flex-col items-start md:items-end gap-2">
<div class="inline-flex rounded-full bg-dark-card border border-dark-border p-1 text-xs">
<button type="button" data-sport-filter="all" class="px-3 py-1.5 rounded-full bg-accent-blue text-white transition-smooth">Vše</button>
<button type="button" data-sport-filter="football" class="px-3 py-1.5 rounded-full bg-dark-card text-gray-300 hover:bg-dark-border transition-smooth">Fotbal</button>
<button type="button" data-sport-filter="futsal" class="px-3 py-1.5 rounded-full bg-dark-card text-gray-300 hover:bg-dark-border transition-smooth">Futsal</button>
</div>
<div class="text-[11px] text-gray-500">20 log na stránku • řazeno: nejnovější</div>
</div>
</div>
<div id="allLoading" class="text-center py-12">
+134 -4
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
// Configuration
@@ -10,8 +11,61 @@ const FACR_API_URL = 'https://facr.tdvorak.dev'
const clubSearch = document.getElementById('clubSearch')
const searchResults = document.getElementById('searchResults')
const uploadSection = document.getElementById('uploadSection')
const clubSportFilterButtons = document.querySelectorAll('[data-club-sport-filter]')
const selectedClubSummary = document.getElementById('selectedClubSummary')
const selectedClubNameEl = document.getElementById('selectedClubName')
const selectedClubTypeEl = document.getElementById('selectedClubType')
const selectedClubCityEl = document.getElementById('selectedClubCity')
const selectedClubWebsiteEl = document.getElementById('selectedClubWebsite')
const selectedClubLogoEl = document.getElementById('selectedClubLogo')
let searchTimeout
let activeIndex = -1
let lastClubs = []
let clubSportFilter = 'all'
function normalizeText(s) {
return (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
}
function highlight(text, query) {
const t = String(text || '')
const nq = normalizeText(query)
if (!nq) return t
const nt = normalizeText(t)
const idx = nt.indexOf(nq)
if (idx === -1) return t
let i = 0, oi = 0, start = -1, end = -1
while (oi < t.length && i <= idx + nq.length) {
const ch = t[oi]
const n = normalizeText(ch)
if (i === idx) start = oi
if (n) i += n.length
oi += 1
if (i >= idx + nq.length) { end = oi; break }
}
if (start === -1 || end === -1) return t
return t.slice(0, start) + '<span class="bg-accent-blue/20">' + t.slice(start, end) + '</span>' + t.slice(end)
}
function updateActive() {
const items = searchResults.querySelectorAll('.club-result')
items.forEach((el, i) => {
if (i === activeIndex) el.classList.add('ring-2', 'ring-accent-blue')
else el.classList.remove('ring-2', 'ring-accent-blue')
})
}
function updateClubSportFilterButtons() {
if (!clubSportFilterButtons || !clubSportFilterButtons.length) return
clubSportFilterButtons.forEach(btn => {
const value = (btn.dataset.clubSportFilter || 'all').toLowerCase()
const isActive = value === clubSportFilter
btn.classList.toggle('bg-accent-blue', isActive)
btn.classList.toggle('text-white', isActive)
btn.classList.toggle('bg-dark-bg', !isActive)
btn.classList.toggle('text-gray-300', !isActive)
})
}
clubSearch.addEventListener('input', (e) => {
clearTimeout(searchTimeout)
@@ -27,6 +81,27 @@ clubSearch.addEventListener('input', (e) => {
}, 300)
})
clubSearch.addEventListener('keydown', (e) => {
const total = searchResults.querySelectorAll('.club-result').length
if (!total) return
if (e.key === 'ArrowDown') {
e.preventDefault()
activeIndex = (activeIndex + 1) % total
updateActive()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
activeIndex = (activeIndex - 1 + total) % total
updateActive()
} else if (e.key === 'Enter') {
e.preventDefault()
if (activeIndex >= 0 && activeIndex < total) {
const item = searchResults.querySelectorAll('.club-result')[activeIndex]
const btn = item.querySelector('.select-club')
if (btn) btn.click(); else item.click()
}
}
})
async function searchClubs(query) {
searchResults.innerHTML = '<div class="text-center py-4"><div class="spinner mx-auto"></div></div>'
@@ -44,7 +119,8 @@ async function searchClubs(query) {
}
const clubs = await response.json()
await displaySearchResults(clubs)
lastClubs = Array.isArray(clubs) ? clubs : []
await displaySearchResults(lastClubs)
} catch (error) {
// Suppress console spam from HTML responses
@@ -85,7 +161,22 @@ async function displaySearchResults(clubs) {
// Silently fail - this is optional data
}
searchResults.innerHTML = clubs.map(club => {
const q = clubSearch.value.trim()
const nq = normalizeText(q)
let filtered = Array.isArray(clubs) ? clubs : []
if (nq) {
filtered = filtered.filter(c => {
const name = normalizeText(c.name)
const city = normalizeText(c.city)
const id = String(c.id || '').toLowerCase()
return name.includes(nq) || city.includes(nq) || id.includes(q.toLowerCase())
})
}
if (clubSportFilter && clubSportFilter !== 'all') {
filtered = filtered.filter(c => (c.type || '').toLowerCase() === clubSportFilter)
}
activeIndex = -1
searchResults.innerHTML = filtered.map(club => {
// Check if we have this logo in our API
const existingLogo = existingLogos.find(l => l.id === club.id)
@@ -120,12 +211,13 @@ async function displaySearchResults(clubs) {
`
}
const clubData = { ...club, display_logo_url: logoUrl }
return `
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer" data-club='${JSON.stringify(club)}' data-logo-url='${logoUrl}'>
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer" data-club='${JSON.stringify(clubData)}'>
<div class="flex items-center gap-4">
${logoHtml}
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-lg truncate">${club.name}</h3>
<h3 class="font-semibold text-lg truncate">${highlight(club.name, q)}</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>` : ''}
@@ -168,6 +260,27 @@ function selectClub(club) {
document.getElementById('clubName').value = club.name
document.getElementById('clubType').value = club.type || 'football'
document.getElementById('clubWebsite').value = club.website || ''
// Update summary card
if (selectedClubSummary && selectedClubNameEl && selectedClubTypeEl && selectedClubCityEl && selectedClubWebsiteEl && selectedClubLogoEl) {
selectedClubNameEl.textContent = club.name || ''
selectedClubTypeEl.textContent = (club.type || 'football').toUpperCase()
selectedClubCityEl.textContent = club.city || ''
if (club.website) {
selectedClubWebsiteEl.innerHTML = `<a href="${club.website}" target="_blank" class="hover:underline">${club.website}</a>`
} else {
selectedClubWebsiteEl.textContent = ''
}
const displayLogo = club.display_logo_url || club.logo_url || ''
if (displayLogo) {
selectedClubLogoEl.innerHTML = `<img src="${displayLogo}" alt="${club.name || ''}" class="max-w-full max-h-full object-contain rounded-md">`
} else {
selectedClubLogoEl.textContent = '🏟️'
}
selectedClubSummary.classList.remove('hidden')
}
// Show upload section
uploadSection.classList.remove('hidden')
@@ -186,6 +299,23 @@ function selectClub(club) {
showNotification(`Vybráno: ${club.name}`, 'success')
}
if (clubSportFilterButtons && clubSportFilterButtons.length) {
updateClubSportFilterButtons()
clubSportFilterButtons.forEach(btn => {
btn.addEventListener('click', () => {
const value = (btn.dataset.clubSportFilter || 'all').toLowerCase()
if (value === clubSportFilter) return
clubSportFilter = value
updateClubSportFilterButtons()
if (lastClubs.length) {
displaySearchResults(lastClubs)
} else if (clubSearch.value.trim().length >= 2) {
searchClubs(clubSearch.value.trim())
}
})
})
}
// ==================== Website Search ====================
const searchWebsiteBtn = document.getElementById('searchWebsite')
+1
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
+1
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
// Configuration
+29
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
const API_BASE_URL = '/api'
@@ -7,12 +8,14 @@ const loading = document.getElementById('allLoading')
const empty = document.getElementById('allEmpty')
const loadMoreBtn = document.getElementById('loadMoreBtn')
const searchInput = document.getElementById('allLogoSearch')
const sportFilterButtons = document.querySelectorAll('[data-sport-filter]')
let page = 1
const limit = 20
let query = ''
let isLoading = false
let hasMore = true
let sport = 'all'
async function loadPage(reset = false) {
if (isLoading) return
@@ -34,6 +37,7 @@ async function loadPage(reset = false) {
url.searchParams.set('limit', String(limit))
url.searchParams.set('page', String(page))
if (query) url.searchParams.set('q', query)
if (sport && sport !== 'all') url.searchParams.set('sport', sport)
const resp = await fetch(url.toString().replace(window.location.origin, ''))
if (!resp.ok) throw new Error('Failed to fetch logos')
@@ -88,6 +92,18 @@ function appendCards(items) {
grid.insertAdjacentHTML('beforeend', html)
}
function updateSportFilterButtons() {
if (!sportFilterButtons || !sportFilterButtons.length) return
sportFilterButtons.forEach(btn => {
const value = btn.dataset.sportFilter || 'all'
const isActive = value === sport
btn.classList.toggle('bg-accent-blue', isActive)
btn.classList.toggle('text-white', isActive)
btn.classList.toggle('bg-dark-card', !isActive)
btn.classList.toggle('text-gray-300', !isActive)
})
}
grid.addEventListener('click', async (e) => {
const delBtn = e.target.closest('.delete-logo')
if (delBtn) {
@@ -125,6 +141,19 @@ searchInput.addEventListener('input', () => {
}, 300)
})
if (sportFilterButtons && sportFilterButtons.length) {
updateSportFilterButtons()
sportFilterButtons.forEach(btn => {
btn.addEventListener('click', () => {
const value = btn.dataset.sportFilter || 'all'
if (value === sport) return
sport = value
updateSportFilterButtons()
loadPage(true)
})
})
}
loadMoreBtn.addEventListener('click', () => {
if (hasMore) loadPage(false)
})
+80 -12
View File
@@ -1,4 +1,5 @@
import './style.css'
import './theme.js'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
@@ -100,6 +101,42 @@ uploadBtn.addEventListener('click', () => {
const searchInput = document.getElementById('searchInput')
const searchResults = document.getElementById('searchResults')
let searchTimeout
let activeIndex = -1
let currentClubs = []
function normalizeText(s) {
return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
}
function highlight(text, query) {
const t = String(text)
const nq = normalizeText(query)
if (!nq) return t
const nt = normalizeText(t)
const idx = nt.indexOf(nq)
if (idx === -1) return t
let i = 0, oi = 0, start = -1, end = -1
while (oi < t.length && i <= idx + nq.length) {
const ch = t[oi]
const n = normalizeText(ch)
if (i === idx) start = oi
if (n) i += n.length
oi += 1
if (i >= idx + nq.length) { end = oi; break }
}
if (start === -1 || end === -1) return t
return t.slice(0, start) + '<span class="bg-accent-blue/20">' + t.slice(start, end) + '</span>' + t.slice(end)
}
function updateActive() {
const items = searchResults.querySelectorAll('.club-result')
items.forEach((el, i) => {
if (i === activeIndex) {
el.classList.add('ring-2', 'ring-accent-blue')
} else {
el.classList.remove('ring-2', 'ring-accent-blue')
}
})
}
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout)
@@ -110,12 +147,31 @@ searchInput.addEventListener('input', (e) => {
return
}
// Debounce search
searchTimeout = setTimeout(() => {
searchClubs(query)
}, 300)
})
searchInput.addEventListener('keydown', (e) => {
const total = searchResults.querySelectorAll('.club-result').length
if (!total) return
if (e.key === 'ArrowDown') {
e.preventDefault()
activeIndex = (activeIndex + 1) % total
updateActive()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
activeIndex = (activeIndex - 1 + total) % total
updateActive()
} else if (e.key === 'Enter') {
e.preventDefault()
if (activeIndex >= 0 && activeIndex < total) {
const item = searchResults.querySelectorAll('.club-result')[activeIndex]
item.click()
}
}
})
async function searchClubs(query) {
searchResults.innerHTML = '<div class="text-center py-4"><div class="spinner mx-auto"></div></div>'
@@ -129,12 +185,19 @@ async function searchClubs(query) {
}
const data = await response.json()
displaySearchResults(data)
const nq = normalizeText(query)
const filtered = data.filter(c => {
const name = normalizeText(c.name || '')
const city = normalizeText(c.city || '')
const id = String(c.id || '').toLowerCase()
return name.includes(nq) || city.includes(nq) || id.includes(query.toLowerCase())
})
displaySearchResults(filtered, query)
} catch (error) {
console.log('Backend not available, showing demo data')
// Demo data when backend is not ready
displaySearchResults(getDemoClubs(query))
const demo = getDemoClubs(query)
displaySearchResults(demo, query)
}
}
@@ -166,12 +229,15 @@ function getDemoClubs(query) {
}
]
return demoClubs.filter(club =>
club.name.toLowerCase().includes(query.toLowerCase())
)
const nq = normalizeText(query)
return demoClubs.filter(club => {
const name = normalizeText(club.name)
const city = normalizeText(club.city)
return name.includes(nq) || city.includes(nq)
})
}
function displaySearchResults(clubs) {
function displaySearchResults(clubs, query) {
if (clubs.length === 0) {
searchResults.innerHTML = `
<div class="text-center py-8 text-gray-400">
@@ -181,12 +247,14 @@ function displaySearchResults(clubs) {
return
}
searchResults.innerHTML = clubs.map(club => `
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer">
activeIndex = -1
currentClubs = clubs
searchResults.innerHTML = clubs.map((club, idx) => `
<div class="club-result bg-dark-bg rounded-lg p-4 border border-dark-border hover:border-accent-blue transition-smooth cursor-pointer" data-index="${idx}">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="font-semibold text-lg">${club.name}</h3>
<p class="text-sm text-gray-400">${club.city || 'N/A'}${club.type || 'football'}</p>
<h3 class="font-semibold text-lg">${highlight(club.name, query)}</h3>
<p class="text-sm text-gray-400">${highlight(club.city || 'N/A', query)}${club.type || 'football'}</p>
<p class="text-xs text-gray-500 font-mono mt-1">${club.id}</p>
</div>
<button
+45
View File
@@ -97,3 +97,48 @@ body {
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Light/Dark theme overrides */
.theme-dark body {
background-color: #0a0e1a;
color: #f9fafb;
}
.theme-light body {
background-color: #f9fafb;
color: #020617;
}
.theme-light .bg-dark-bg {
background-color: #f9fafb;
}
.theme-light .bg-dark-card {
background-color: #ffffff;
}
.theme-light .border-dark-border {
border-color: #e5e7eb;
}
.theme-light .text-gray-400 {
color: #6b7280;
}
.theme-light .text-gray-500 {
color: #6b7280;
}
.theme-light .text-gray-600 {
color: #4b5563;
}
.theme-light .spinner {
border-color: rgba(15, 23, 42, 0.08);
border-top-color: #3b82f6;
}
.theme-light .bg-dark-card\/50 {
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
}
+72
View File
@@ -0,0 +1,72 @@
// Global light/dark theme handling for Czech Clubs Logos frontend
const THEME_KEY = 'clublogos-theme'
function getPreferredTheme() {
try {
const stored = localStorage.getItem(THEME_KEY)
if (stored === 'light' || stored === 'dark') return stored
} catch (_) {}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light'
}
return 'dark'
}
function applyTheme(theme) {
const root = document.documentElement
const mode = theme === 'light' ? 'light' : 'dark'
root.classList.remove('theme-light', 'theme-dark', 'dark')
if (mode === 'light') {
root.classList.add('theme-light')
} else {
root.classList.add('theme-dark', 'dark')
}
try {
localStorage.setItem(THEME_KEY, mode)
} catch (_) {}
const toggle = document.getElementById('themeToggle')
if (toggle) {
if (mode === 'light') {
toggle.innerHTML = `
<span class="inline-flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
<span class="hidden sm:inline">Tmavý režim</span>
</span>
`
} else {
toggle.innerHTML = `
<span class="inline-flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v2m0 14v2m9-9h-2M5 12H3m15.364-6.364l-1.414 1.414M8.05 17.95l-1.414 1.414m0-12.728L8.05 8.05m9.9 9.9l-1.414-1.414M12 8a4 4 0 100 8 4 4 0 000-8z" />
</svg>
<span class="hidden sm:inline">Světlý režim</span>
</span>
`
}
}
}
function setupThemeToggle() {
const toggle = document.getElementById('themeToggle')
if (!toggle) return
toggle.addEventListener('click', () => {
const isLight = document.documentElement.classList.contains('theme-light')
applyTheme(isLight ? 'dark' : 'light')
})
}
if (typeof window !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
const initial = getPreferredTheme()
applyTheme(initial)
setupThemeToggle()
})
}